线程安全是指多个线程访问同一代码,不会产生不确定的结果;
一、线程同步
为了避免多线程在共享资源时发生冲突,所以 要在线程使用该资源时,就为线程上一把“锁”,第一个访问资源的线程为资源上锁,其他线程若想访问该资源,则必须等待解锁为止,解锁的同时,另一个线程访问资源并为资源上锁。
在使用多线程编程时,一般解决同步问题的方法有三种:
- 用synchronized关键字;
- wait()方法和notify()或notifyAll()方法;
- 还有一种非常灵活简便的同步锁可以选择,可以通过java.util.concurrent.locks.ReentrntLock类 的对象调用lock()方法来实现加锁操作,此时可以进行同步操作,同步操作结束后,调用unlock()释放同步锁;
1、synchronized:
重量级锁, 与操作系统交互,线程从用户态切换到内核态
首先了解synchronized和volatile的作用和区别;,这里再补充一点,那就是volatile具有可见性和有序性,却不具有原子性,那它为何不具有原子性呢?看下面这个例子,
一个变量i被volatile修饰,两个线程想对这个变量修改,都对其进行自增操作也就是i++,i++的过程可以分为三步,首先获取i的值,其次对i的值进行加1,最后将得到的新值写会到缓存中。
线程A和线程B都拿到了 i = 100 的一个副本到自己的工作内存中去,并且在CPU中同时执行 i++这个操作,A先完成 i++ 这个操作,并将CPU寄存器中的运算结果回写到线程A的工作内存中去,然后 i = 101,这个时候根据缓存一致性协议,系统会检测到这个 i 的值变成了101,也就是volatile的可见性,系统会将其他地方的i置为无效,并且立即将线程A工作内存中的 i = 101回写到主内存中去,线程B重新从主内存中获取 i = 101,但是对于线程B来说,自己已经在CPU中执行过这个 i++操作了,代码已经走过了,也就是他不会在执行一次了,那么最终的结果就是101,而我们期望的是102。
i++这个操作其实是三个原子操作的组合操作,读取、修改、写回内存,只要在线程B未将+1后更新的值写入内存之前,线程A完成了读取这个原子操作,那么线程A和线程B拿到的就是同一个值,然后再分别进行的自增、回写操作;
所以,当你启动多个线程来对volatile修饰的i进行 i++ 这个操作的时候,总的结果总是小于我们期望的结果,例如:
public class Test {
private volatile static int sum = 0;
public static void main(String[] args) {
//启动10个线程
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(() -> {
//每个线程自增10000次
for (int j = 0; j < 10000; j++) {
sum++;
}
});
//启动线程
thread.start();
}
try {
//主线程睡眠3s,等待上面两个线程执行完毕在输出结果
Thread.sleep(3000);
System.out.println(sum);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
这样10个线程每个线程自增10000次,本来的结果应该是100000,但是我们得到的结果却小于这个数:
第一次执行:
第二次:
…
原理如下图:
注意,这个时候对于线程A来说,已经执行了一次 ++操作,对于B线程来说,它也进行了一次++操作,总共已经执行了 2 次++操作了!
如果这个时候输出结果,是1,就没有达到我们的预期!大概过程就是这样,所以我们多个线程操作的时候结果总是比预期的小!
synchronized的使用:
- ①修饰代码块------>作用于当前代码块
- ②修饰普通方法------>作用于对象
- ③修饰static方法----->作用于类
synchronized的底层实现原理:
- synchronized修饰代码块,底层提供了monitorEnter和monitorexit(结束的时候会有两个monitorexit防止第一个因为异常退出而没有释放锁);
- synchronized修饰普通方法和静态方法不是通过monitorEnter和monitorexit机制来处理的,而是通过底层flag标志上ACC_SYNCHRONIZED的来标识,顺便说一下volatile的底层实现,在程序的字节码文件中,加了volatile修饰的变量会多一个ACC_VOLATILE,在底层汇编语言上就是讲该变量前添加Lock#前缀。;
- 线程的同步,无论采用哪种方式,其本质是对一个对象的监视器(monitor)进行获取,而这个获取过程是排他的,也就是同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获取到监视器(执行该方法)的线程将会被阻塞在同步块和同步方法的入口处,进入BLOCKED状态。底层都是通过获取monitor对象来获取锁,而monitor对象是由底层操作系统提供的mutex锁机制来完成对象获取锁和释放锁;(monitor是对象监视器);
以下图例是对象、监视器对象同步队列和执行线程之间的关系:
可以看到,任意线程对Object(Object由synchronized保护)的访问,首先要获得
Object的监视器。如果获取失败,线程进入同步队列,线程状态变为BLOCKED(阻塞)。当访问Object的前驱(获得了锁的线程)释放了锁,则该释放操作唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。
下面是一段加了synchronized关键字的代码以及它的字节码文件:
public class TestDemo1 {
private int a = 0;
public int getA() {
synchronized (this) {
int x = a;
a++;
return x;
}
}
public static void main(String[] args) {
TestDemo1 demo1 = new TestDemo1();
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" "+demo1.getNext());
}
}).start();
}
}
}
二、线程的通信
wait()方法和notify()或notifyAll()
如果没有wait()方法和notify()或notifyAll()这些方法,那我们:
- ①父线程给子线程传递信息--------> 构造函数的时候传递
- ②子线程如何给父线程传递信息----------> FutureTask返回结果的
这样是非常不方便的,于是java给我们提供了wait()方法和notify()或notifyAll(),这些方法是Object类的方法,所有每个对象都有三个方法;
方法介绍:
方法名 | 作用 |
---|---|
wait() | 属于Object的方法,任何对象都可以调用wait()方法,调用wait()会使当前线程阻塞住,直至其他线程调用notify()或者notifyAll(),当前线程才能继续执行;wait()方法必须在锁中(synchronized)使用,否则报错;调用wait()当前线程会释放锁供使其他线程使用; |
notify() | 通知其中调用wait()方法的任意一个线程来继续执行 |
notifyAll() | 通知调用wait()方法的所有线程 |
注意,为什么用 wait()和notify()这些方法必须要上锁呢,这是因为你既然要等,就要明白在等什么,这里等待的就是一个对象发出的信号,所以它一定要基于对象存在,既然是基于对象存在的,那么它不得不用一个数据结构来存放这些等待的线程,而且这个数据结构应该是与该对象绑定的(其实这个数据结构就是一个双向链表),此时在这个对象上可能同时存在多个对象调用wait()或notify()方法,在向这个双向链表进行写入删除等操作时,依然操作并发问题,理论上也需要一个锁来控制,在JVM的内核源码中并没有发现任何自己用锁来控制写入的操作,只是通过检查当前线程是否为对象的owner来判断是否需要抛出异常,由此可见它希望该动作由java程序这个抽象层次来控制,那它为什么不想自己控制锁呢?因为有时候更低层次的锁并不是好事,因为这样的请求外部可能是反复循环的去征用,或者这些代码还可能在其他地方复用,也许将它粗粒度化更好一些,而且这样的出现写在java出现中本身也会更加清晰,更容易看出他们之间的相互关系;
为了更充分的了解这些方法,我们需要了解锁池和等待池:
锁池(同步队列):存放竞争当前锁失败的线程;假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。
等待池(等待队列):存放调用wait()方法的线程;调用notify会让等待池里的线程放到锁池里面让他们去争夺CPU; 假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁后,进入到了该对象的等待池中;
下面再来说说再说notify和notifyAll的区别:
- ①线程调用了对象的 wait()方法,便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁;
- ②notifyAll调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争;notify调用后,只会将等待池中的一个随机线程移到锁池。
注意,只有调用了wait()方法才会释放锁,调用notify、notifyAll并没有释放锁,仅仅通知其他调用了wait()方法的线程可以去抢锁了而已;
关于线程通信的一个实例:
生产者、消费者模型:
代码的某个模块负责生产数据(供货商),而生产出来的数据却不得不交给另一模块(消费者)来对其进行处理,在这之间我们必须要有一个类似上述超市的东西来存储数据(超市),这就抽象除了我们的生产者/消费者模型。
其中,产生数据的模块,就形象地称为生产者;而处理数据的模块,就称为消费者;生产者和消费者之间的中介就叫做缓冲区(或者叫仓库)。
下面是一个生产者、消费者模型的简单实现代码:
/**
* @ClassName Warehouse
* @Description 仓库类
* @Author lzq
* @Date 2019/4/20 21:47
* @Version 1.0
**/
public class Warehouse {
private ArrayDeque<Integer> deque = new ArrayDeque<>(); //用来存储数据的队列
private final int MAX_COUNT = 5;
/**
* 添加元素(模拟生产者生产)
* @param value
*/
public synchronized void push(int value) {
try {
if(deque.size() >= MAX_COUNT) {
System.out.println("库存已满,生产者暂停生产......");
wait();
}
deque.addLast(value);
System.out.println("生产者 "+Thread.currentThread().getName()+" 生产,当前加入的数据为:"+value);
notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 去除队头元素,模拟消费者消费
*/
public synchronized void poll() {
try {
if(deque.size() <= 0) {
System.out.println("库存为空,消费者停止消费......");
wait();
}
int x = deque.pollFirst();
System.out.println("消费者 "+Thread.currentThread().getName()+" 消费,当前取出的数据为:"+x);
notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* @ClassName Producer
* @Description 生产者
* @Author lzq
* @Date 2019/4/20 22:00
* @Version 1.0
**/
public class Producer extends Thread{
private Warehouse warehouse;
public Producer(Warehouse warehouse) {
this.warehouse = warehouse;
}
Random random = new Random();
@Override
public void run() {
while (true) {
int x = random.nextInt(10)+1;
warehouse.push(x);
try {
Thread.sleep(1000); //休眠1s
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* @ClassName Consumer
* @Description 消费者
* @Author lzq
* @Date 2019/4/20 22:02
* @Version 1.0
**/
public class Consumer extends Thread{
private Warehouse warehouse;
public Consumer(Warehouse warehouse) {
this.warehouse = warehouse;
}
@Override
public void run() {
while (true) {
warehouse.poll();
try {
Thread.sleep(1000); //取出后休眠1s
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
测试代码:
public static void main(String[] args) {
Warehouse warehouse = new Warehouse();
Thread t1 = new Producer(warehouse);
Thread t2 = new Producer(warehouse);
Thread t3 = new Producer(warehouse);
Thread t4 = new Consumer(warehouse);
Thread t5 = new Consumer(warehouse);
Thread t6 = new Consumer(warehouse);
t1.setName("生产者1");
t2.setName("生产者2");
t3.setName("生产者3");
t4.setName("消费者1");
t5.setName("消费者2");
t6.setName("消费者3");
t1.start();
t2.start();
t3.start();
t4.start();
t5.start();
t6.start();
}
运行结果:
一定要注意啊,在一个synchronized修饰的方法里面用wait()或者notify()之类的方法不需要在前面拿指定的对象去调,默认的就是当前对象,但是synchronized修饰代码块的时候一定要注意,拿synchronized锁住的那个对象去调用wait()或者notify(),否则就会出现异常的。。。。。。