概要前提
线程之间 内存资源是共享的, 这里的“内存共享”指多个线程可以访问和操作同一块内存区域中的数据,
这种共享可以是全局变量、静态变量(static),也可以是堆上的对象等
正是因为线程之间 内存资源是共享, 所以多个线程在对同一个内存资源
进行读或者写的是会造成 数据不一致性 ,也就是
当一个线程读取一个变量的值时,该变量的值可能在另一个线程中被修改了,从而导致读取到的是一个不一致或旧的值
由于cpu对线程的调度不可预估性,
为了避免问题存在,Java提供了多种同步机制,如synchronized关键字、ReentrantLock锁、volatile关键字
synchronized使用
synchronized 可以保证 多个线程在对同一个共享资源操作的时候 实现
可见性,原子操作,有序性
synchronized 锁的是对象
synchronized关键字可以用在方法上,也可以用在代码块上,
不管怎么变,原则都是 拿一个 对象 当锁
- 对于修饰方法的synchronized,默认的锁对象就是当前方法的对象。
Object a=new Object();
a.doSomething();
//用a 对象当锁
public synchronized void doSomething() {
}
- 对于修饰静态方法的synchronized,其锁对象就是此方法所对应的类Class
private static synchronized void method() {
}
- 用任何一个对象当锁同步代码块
Object a=new Object();
synchonized(a) {
//do something here
}
因为对象锁只有一个,那么哪个线程争抢到这个对象锁,哪个线程下的就先执行,synchonized{}内的代码只有一个线程能做,而synchonized{}外的就不是了,当 synchonized{} 执行完后 或者抛出了异常 会自动把该线程持有的锁释放。
wait 和 notify
wait()和 notify() 是每个对象的方法
即Object object= new Object();
object.notifyAll(),object.wait(),object.notify。
当执行 对象的wait(),这个时候当前线程会处于一个挂起等待状态,
wait()后面的代码都不会执行,并且,会把该线程持有的锁给释放出去,直到 有对象执行 唤醒 方法,
调用 wait()后 需要有人执行唤醒 notify ,才能继续执行
而notify 就是唤醒,但是 并不是也不百分百 调用了 notify 那些 wait 的地方就立马 继续跑下去 ,
notify 只是让 wait 的线程 从挂起状态转为 就绪等待状态 ,等待cpu调度 当该线程再次争抢到锁,wait()后面的代码才会继续执行。
sleep和wait
调用 sleep 和 wait 都会让当前线程挂起阻塞
sleep 时间到了就唤醒
而 wait 需要 对应 对象锁 执行 唤醒notify 操作才会唤醒
调用 wait 会释放锁,而sleep不会跟对象锁没关系
synchronized 的使用场景
synchronized 的使用场景思路 其实都是 互斥访问
互斥访问就是通过哪个线程先拿到锁,哪个线程先对资源读或者写,而另外的其他线程在调用 对 资源数据 读写的时刻
线程会挂起阻塞在此地,直到获取到锁才会继续唤醒执行下去
我们希望两个线程在做事情的时候,其中一个线程 先执行,而另外一个线程挂起阻塞等待
当 A B 两个线程开启 由于cpu 调度 我们无法预约 谁先执行,
也就是会出现两种 情况 ,可能A先 ,可能 B 先
首先不管如何, 一方先拿到锁执行,另外则会再锁外线程挂起阻塞等待
我们希望 A 先执行 再执行B, 还没执行A 前,另外的B线程挂起等待
这个时候,可以通过 在 B 中 进行 条件 循环 wait , 由于条件满足是再
A 中实现, 所以 B 会因为 条件无法达成了 wait 等待下去
这个时候 锁就会 给到 A ,在 A中进行条件达成,然后再 notify
也就是 释放锁 , 让B 接着执行
这样就算 线程 B 先执行,它也会因为 条件无法达成 wait 阻塞挂起
而原本因没法抢到锁而挂起等待的A 会拿到锁
只有A 执行条件达成,notify ,B 才会继续执行
而如果 线程 A 先执行,线程B 也会在锁外方法 阻塞等待下去
说白了
其实就是希望慢执行的一方 如果被cpu调用先执行了,调用wait 先等着
等 希望先执行的人执行了 调用 notify 通知你 你再去执行
应用场景如下:
有时候我们 写代码的时候 在主线程开启一个线程去做事情,然后希望上面的代码先执行 ,下面的代码在上面的代码获取结果 后才执行
//在主线程中开启一个线程做事情
val myThread = MyThread()
myThread.start()
// 但是在这里,你无法保证,线程的中任务先执行,然后下面的获取数据才执行, 很有可能下面的主线程获取资源执行了,它获取的数据是null
//在这里是可以简单的使用 sleep (1000) 让 主线程 停止等待下,等上面执行完 然后下面才执行,但是还有更好方法
myThread.getResult()
public class MyThread extends Thread {
String result;
@Override
public void run() {
super.run();
result = "result";
}
}
String getResult() {
return result;
}
}
那就是采用 互斥访问
public class MyThread extends Thread {
String result;
@Override
public void run() {
super.run();
Log.d("MyThread", "run: 1");
synchronized (this){
sleep(2000);
result = "result";
Log.d("MyThread", "run: 2");
notifyAll();
}
}
String getResult() throws InterruptedException {
Log.d("MyThread", "getResult: 1");
if(!isAlive()){
return null;
}
synchronized(this){
Log.d("MyThread", "getResult: 2");
while (result == null){
Log.d("MyThread", "getResult: 3");
wait();
}
}
Log.d("MyThread", "getResult: 4");
return result;
}
}
val myThread = MyThread()
myThread.start()
myThread.getResult()
Log.d("activity", "获取资源成功")
我们希望的是 线程里面 获取到资源 是最先执行, 然后 主线程获取资源的时候才执行
在这里 通过 两个 synchronized ,进行了两个 synchronized{}块内的互斥访问
先是利用 myThread 这个对象锁 ,对子线程的 synchronized {} 和主线程的 synchronized {} 谁先抢到锁,谁就先执行
有两种情况:
- 如果是 子线程的 synchronized {} 先拿到锁,那么当 主线程 调用 getResult
方法的时候,它会停在锁synchronized {}外 等待获取,,这个时候主线程是卡住的,你会发现主线程的
Log.d(“activity”, “获取资源成功”) 这句打印是 得 等到 主线程 myThread.getResult()
获取锁 之后才会执行完
- 如果 是 主线程的 synchronized {} 先拿到锁,那么他会因为没有拿到资源,资源 ==null ,在 wait(),也就是主线程会停在 wait 这里,而这个时候 锁会给到子线程的 synchronized {} 获取资源,然后 ,获取完资源后,调用了notifyAll() 唤醒主线程 wait ,也就是 wait 后面就会继续执行,而由于获取到资源了,所以不再死循环 所以就返回 资源 最终主线程获取成功这一句话才会打印
这样不管cpu 如何调度 谁先获取锁,都能保证 子线程获取到资源 是最先执行, 然后 主线程获取资源的时候才执行
- 生产者消费者模型
下面我们以一个最经典的生产消费模式来感受下。
我们希望 生成一个数据,再消费一个数据,在没有生产之前,消费等待生产,在生产了一个之后,等到消费再生产
class FactoryBread {
var count:Int=0
@Synchronized
fun make(threadName: String) {
while (count == 0) {
count++
println(threadName + "生产完成面包" + "当前面包数量为:" + count)
println("生产完成告诉消费者可以消费了")
(this as Object).notifyAll()
try {
println("生产者等待消费者消费")
(this as Object).wait()
} catch (e: InterruptedException) {
throw java.lang.RuntimeException(e)
}
}
}
@Synchronized
fun consume(threadName: String) {
while (true) {
if(count>=1){
count--
println(threadName + "消费完成面包" + "当前面包数量为:" + count)
(this as Object).notifyAll()
}
try {
println("消费者等待生产中")
(this as Object).wait()
} catch (e: InterruptedException) {
throw java.lang.RuntimeException(e)
}
}
}
}
fun main() {
val factoryBread = FactoryBread()
Thread{
factoryBread.make(Thread.currentThread().name)
}.start()
Thread{
factoryBread.consume(Thread.currentThread().name)
}.start()
}
上面是以 factoryBread 这个对象当作对象锁, Synchronized 操作在方法中
当两个线程执行的时候,
消费者线程方法先执行,也会因为 count 没有 >=1 调用 wait 释放 锁 等待唤醒,
然后 生产者 生产完数据后 就会调用 notify 通知消费者可以执行,并且
自己调用 wait 释放锁,并且等待消费者唤醒
消费者因为条件满足了 count ==1 ,那么进行消费,而消费结束了,
调用 notify 通知 生产者不再挂起
自己调用 wait 释放锁,并且等待生产者唤醒
而如果是生产者线程先执行,消费者线程方法会阻塞在 Synchronized方法外无法阻塞等待获取锁才会进入
还有一种是 synchronized放在代码块上的
class FactoryBread {
var count:Int=0
//制造一个对象,用于锁
val lock = Object()
fun make(threadName: String) {
synchronized(lock) {
while (count == 0) {
count++
println(threadName + "生产完成面包" + "当前面包数量为:" + count)
println("生产完成告诉消费者可以消费了")
lock.notifyAll()
try {
println("生产者等待消费者消费")
lock.wait()
} catch (e: InterruptedException) {
throw java.lang.RuntimeException(e)
}
}
}
}
//消费面包的工具,参数就是哪个工人消费的。
fun consume(threadName: String) {
synchronized(lock) {
while (count >= 1) {
count--
println(threadName + "消费完成面包" + "当前面包数量为:" + count)
lock.notifyAll()
try {
println("生产者等待消费者消费")
lock.wait()
} catch (e: InterruptedException) {
throw java.lang.RuntimeException(e)
}
}
}
}
}
上面只是写法不同,专门弄个对象锁。
总结:
- 对于多个线程共享一个资源 在对 数据进行 读或者写 的时候 会造成线程不安全问题
- 线程不安全可以用对象锁synchronized 让多个线程在读或者写的时候进行互斥访问
- 互斥访问就是通过哪个线程先拿到锁,哪个线程先对资源读或者写,而另外的其他线程在调用 对 资源数据 读写的时刻
线程会挂起阻塞在此地,直到获取到锁才会继续唤醒执行下去 - 通过 一个线程条件不满足就 wait,等另外线程条件达成调用 notify 的方式 实现顺序执行同步