Java基础(十九)——
一、线程安全(同步锁和监视器)
1、引子——线程不安全
举个例子引入安全锁:
10家店,同时卖1000部电脑。换成代码就是10条线程,争抢一个资源(代码如下):
这样会造成一个很严重的问题,就是会出现多个线程同时争抢一个资源,且存在多个线程同时抢到这个资源:
这种现象就是线程不安全。
2、线程安全问题
线程安全问题:多个线程同时访问异步临界资源,可能会造成数据混乱。
异步:大家一起访问。
解决方法:
1、同步锁:synchronized
1.1 同步代码块:
需要同步的代码放在同步代码块中,添加同步锁和同步监视器要求多个线程访问的时候,使用的是同一个对象的同
步监视器当有线程进入同代码块,其他线程则会等待进入同步代码块的线程执行完毕后才会进入。
1.2 同步方法:
被同步锁修饰的方法为同步方法,同步监视器为方法本身当一条线程调用同步方法时,其他线程则会等待同步方法
执行完再调用,要求方法为静态方法或者使用同一个对象
3、同步锁(同步代码块)解决线程安全
同步代码块:
synchronized ( 这里需要放入同步监视器 ) {
}
之所以要放入同步监视器,是因为这里创建了 10 条线程,每条都是独立的。如果访问的不是同一个对象,各自访问不同的对象,那就毫无意义,实现不了同步。只有所有线程都访问同一个资源,才能实现同步。
一般会定义一个临界资源,充当同步监视器:
注意:这个同步监视器必须为对象,如果是静态常量也不行。
最终代码:
public class Homework {
public static void main(String[] args) {
ExecutorService loop = Executors.newFixedThreadPool(10); // 在线程池开10条线程
Sell sell = new Sell();
for (int i = 1; i < 11; i++) {
loop.submit(sell);
}
loop.shutdown();
}
}
class Sell implements Runnable{
static int num = 1; // 独享一份空间
static final Object obj = new Object();
@Override
public void run() {
while (num < 1001){
// synchronized 这个是同步锁。 obj 这个是监视器
synchronized (obj){
if (num < 1001){
System.out.println("第"+(Thread.currentThread().getId()-10)+"家外星人店出售第"+num+"台电脑");
}
num++;
}
}
System.out.println("第"+(Thread.currentThread().getId()-10)+"家外星人店已售罄");
}
}
注意:锁要放在 if 判断语句的外面。如果放在里面,会出现溢出现象。因为会存在同时有几条线程进来,其中一条线程已经到1000了,而后面排队的线程会继续取资源,接着出现1001这种现象(最多不会超过1000 + 线程数量)。
经测试可知:最终效果实现了,没有出现线程安全问题。
4、同步锁(同步方法)解决线程安全
a、继承方式
只需要在方法返回值前面加个 synchronized ,即是同步方法。不过要注意的是,不能直接加到 run 方法里面,需要另外开个方法。不过此时还不够,因为缺少同步监视器。同步方法监视器就是同步方法本身。 且必须保证每个线程调用的都是同一个方法,所以这个方法是要静态的,才能发挥同步监视器的作用。
代码:
public class Demo01 {
public static void main(String[] args) {
for (int i = 1; i < 11; i++) { // 开启10条线程
new MyThread("第"+i+"家").start();
}
}
}
class MyThread extends Thread{
static int computer = 1;
private boolean flag = true; // 开关。控制循环是否继续。
public MyThread(String name){ // 调用父类有参构造方法,给线程新的名字。因为线程本身能够调用 getName()方法。
super(name);
}
@Override
public void run() { // 重写 run 方法
while (flag){ // while 循环不能加在同步方法里面。
flag = sell();
}
}
// 同步方法的监视器为方法本身,且方法为静态的
public synchronized static boolean sell(){
if (computer<1001){
System.out.println(Thread.currentThread().getName()+"外星人专卖店卖出第"+computer+"台电脑");
computer++;
return true;
}else {
System.out.println(Thread.currentThread().getName()+"外星人专卖店已售罄");
return false;
}
}
b、实现方式
上面是通过继承的方式实现的。如果是实现接口的方式,下面这个地方要改:
这里可以有两个参数。
图解:
5、回顾 StringBuffer 和 StringBuilder
这两个以前提到过,看其底层代码可知哪个线程安全:
结论是:StringBuffer 安全。
二、让线程陷入阻塞的方法
1、了解wait()
前面学过,让线程陷入阻塞的有 sleep(),是通过 Thread 直接调用的。 wait()也能让线程陷入阻塞,通过对象调用。wait()来自 Object 类。
2、wait 和 sleep 在同步代码块内的不同表现
测试,wait 和 sleep 在同步代码块中的不同表现。如果线程陷入阻塞了,其他线程还能进来,说明该线程的资源被释放了;反之,如果不能进来,说明该线程的资源没有被释放,其他线程还在等待。
a、sleep 在同步区域内休眠
测试代码:
class MyThread extends Thread{ // 测试休眠是否会释放资源
public void run(){
synchronized (Demo01.obj){
System.out.println(Thread.currentThread().getName()+"线程A开始休眠");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"线程A结束休眠");
}
}
}
b、wait 在同步区域内等待
class MyThread2 extends Thread{ // 测试等待是否会释放资源
public void run(){
synchronized (Demo01.obj){
System.out.println(Thread.currentThread().getName()+"线程Z开始等待");
try {
Demo01.obj.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"线程Z结束等待");
}
}
}
结果:
可以看到,其他线程进来了。
c、结论——同步区域内
经测试得出结论:
线程在同步区域内进入 休眠 状态,线程不会释放资源。
线程在同步区域内进入 等待 状态,线程会释放资源。
d、wait()必须通过同步监视器的对象进行调用
这里可以看到,都是同一个对象。如果换一个对象,也同样是静态常量,就会报错:
然后运行结果:
报错了。
3、wait 和 sleep 在同步区域外中的不同表现
a、sleep
在同步区域外是否可以休眠。
以往使用 sleep 的时候,就没有同步区域,那时候也没有报错。
b、wait
可以看到,报错了。
c、结论
wait 只能在同步区域中使用。
4、不同对象无法在同步中使用 wait()
准确点说:同一个对象的同步方法里面使用当前对象调用。
这里直接说能够实现的方法:
通过实现接口的方式实现线程,然后通过 this 的方式去调用 wait()方法,才能成功调用,其他方式都不行。
代码:
class MyThread2 implements Runnable{ // 通过实现接口的方式。继承方式不行
@Override
public void run(){
method();
}
private synchronized void method() { // 这里没有 static
System.out.println(Thread.currentThread().getName()+"开始等待");
try {
this.wait(); // 这里只能用 this 。如果是通过类直接调用同步监视器,不行。
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"结束等待");
}
}
5、sleep 和 wait 的各自唤醒方法
直接上结论:
sleep :顾名思义,休眠一段时间会自己唤醒。所以 sleep 会自动唤醒。
wait : 需要 notify() 或者 notifyAll() 来唤醒。其中, notifyAll()是唤醒全部线程;而 notify 是华唤醒一条线程。
a、唤醒一条线程
结果:
b、唤醒全部线程
三、lock——手动上锁(互斥锁/可重入锁)
1、初识 lock
先看下有什么方法:
2、lock 的使用
需要实例化对象:
然后给需要上锁的代码块上锁:
这样,设置好代码以后,就能实现同步的效果。
同样是用卖外星人电脑举例子。这里采用继承的方式创建线程:
public class Demo01 {
public static void main(String[] args) {
for (int i = 1; i < 11; i++) { // 开启10条线程
new MyThread("第"+i+"家").start();
}
}
}
class MyThread extends Thread{
static int computer = 1;
private static ReentrantLock lock = new ReentrantLock(); // 如果通过线程的方式创建线程,为了保证每一条线程用的都是同一个锁,这里需要加 static
public MyThread(String name){ // 调用父类有参构造方法,给线程新的名字。因为线程本身能够调用 getName()方法
super(name);
}
@Override
public void run() { // 重写 run 方法
while (true){
lock.lock(); // 手动上锁
try {
if (computer<1001){
System.out.println(Thread.currentThread().getName()+"外星人专卖店卖出第"+computer+"台电脑");
computer++;
}else {
System.out.println(Thread.currentThread().getName()+"外星人专卖店已售罄");
break;
}
}finally {
lock.unlock(); // 既然最终都要执行这个解锁,就放到 finally 里面
}
}
}
四、公平锁
1、了解公平锁和非公平锁
这里需要解释下公平锁和非公平锁的概念:
公平:每条线程轮流拿取资源,每条线程都有份,就是公平锁。
非公平:可能存在一条线程拿取多个资源,就是非公平锁。非公平锁的线程拿取资源是随机的
下图这种就是不公平的:
电脑都被第二家店铺垄断了。
2、公平锁的创建方式
公平锁的创建方式很简单:
只需要在创建 lock 的时候,里面传递 true 这个参数,就是公平锁了。
3、对公平锁的误解
有时候创建了公平锁,10条线程,但是拿取资源的时候并不是每个资源都轮流拿,这是怎么回事呢?
首先,这里确实是公平锁了。不能出现每条线程轮流拿取资源是因为有的线程太快了,在其他线程还没创建的时候,就立马又回来拿资源。可以手动增加线程的工作时间,跳到一个适当的空隙,就能看到每条线程轮流拿取资源。但要承认的事实是,确实这里已经是公平锁。
举个例子:几个人轮流买奶茶,第一个人买完奶茶走了,但是第二个人离奶茶店还有点距离,第一个人买完就立马转身回来买第二杯奶茶。如果增加第一个人买完奶茶后回奶茶店买奶茶的距离,就能看到轮流买奶茶的景象。就是这个道理。