一.synchronized
1.cynchronized的特性
1)互斥(保证原子性)
synchronized 会起到互斥的效果
- 当一个线程进入 synchronized 修饰的代码块,相当于 加锁
- 同一个线程退出 synchronized 修饰的代码块,相当于 解锁
如果线程一,首先执行到这个方法,首先会得到锁(synchronized),进行加锁.当其他线程执行到这个方法,若是线程一还未执行完毕,也就意味着这个方法还处于加锁状态,此时其他线程就只能阻塞等待.直到线程一完成被 synchronized 修饰的方法进行释放锁.然后其他线程就会去争这个锁,拿到锁才能进行执行,否则就一直阻塞等待.
也就意味着,被 synchronized 修饰的代码块,无法被并发执行,变成了串行.也就保证了原子性,进而能够保证线程安全
static class Counter{
private int i;
// public synchronized void increase(){
// i++;
// }
public void increase(){
//也可以这样包裹代码块,其中 this 表示修饰的对象(代码块)
synchronized (this) {
i++;
}
}
}
关于加锁的对象: Java中任意的对象,都可以作为"锁对象"
如上代码块也可以是一个对象.
2)刷新内存(保证内存的可见性)
例如之前所说的例子,频繁快速进行读取的时候,可能出现内存可见性问题.
经过编译器优化(进行指令重排序)之后,原本是在内存中读取的,被优化成在 CPU(寄存器) 中读取了.
int i = 0;
while(true){
if(i != 0){ //读取 i 进行判断
break;
}
}
我们可以在里面增加打印操作,或者 使用Thread.sleep()使得该线程执行会缓慢些,也能够制止内存可见性问题.
但是,这样子并不完全保证说可以不出现内存可见性问题.
可以使用 synchronized 关键字,保证 i 每次读取都是在内存中读取的
又如下例子的代码:
public void increase(){
i++;
}
for (int i = 0; i < 50_000; i++) {
counter.increase();
}
原本的流程是:①从内存中拿到 i ,放到 CPU 寄存器中 ②进行++操作 ③将 i 放回到内存中
每次流程都应该如此,但是,经过编译器优化(指令重排序)后,就变成了 ①②②②②②②②…③ 了.
编译器优化,能够极大的提高程序效率.虽然我们使用 cynchronized 关键字可以保证每次都在内存中进行读取,但也意味着我们将损耗一定的效率
3)可重入
synchronized 允许一个线程针对一把锁,进行再次加锁
synchronized public void increase(){
synchronized (this) {
i++;
}
}
按道理如果第一次加锁的代码阻塞了,就无法释放这个锁,就可能出现死锁
但是 synchronized 不会出现死锁的情况,他是一个"可重入锁"
在可重入锁的内部,包含了"线程持有者"和"计数器"两个信息:
- 如果某个线程加锁的时候, 发现锁已经被人占用, 但是恰好占用的正是自己, 那么仍然可以继续获取
到锁, 并让计数器自增 - 解锁的时候计数器递减为 0 的时候, 才真正释放锁. (才能被别的线程获取到 )
二.volatile
1.保证内存的可见性
除了 synchronized 之外,volatile 也能够保证内存的可见性,但无法保证原子性,这是二者之间的区别
static class Counter{
volatile static int flg = 1;
}
public static void main(String[] args) {
Thread t = new Thread(){
@Override
public void run() {
System.out.println("线程开始");
while(true){
// System.out.println(Counter.flg);
if(Counter.flg != 1) {
break;
}
}
System.out.println("线程终止");
}
};
t.start();
try {
Thread.sleep(3000);
System.out.println("即将终止线程");
Counter.flg = 0;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
具体使用情况具体分析,如果只需要保证内存可见性,那么就只需要使用 volatile 即可
2.JMM(java memory model) 内存模型:
当读取一个变量的时候,不一定真的是在内存中读取,也有可能这个数据已经在 CPU 或者 cache 中缓存着了,这个时候,编译器就可以进行优化(指令重排序),绕过内存,直接从 CPU 或者 cache 中读取这个数据.
**Java虚拟机规范中定义了Java内存模型.
目的是屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果 **
JMM 就把 CPU 的寄存器,L1,L2,L3 cache 统称为"工作内存"
JMM 也把真正的内存成为"主内存"
- 线程之间的共享变量存在 主内存 (Main Memory).
- 每一个线程都有自己的 “工作内存” (Working Memory) .
- 当线程要读取一个共享变量的时候, 会先把变量从主内存拷贝到工作内存, 再从工作内存读取数据.
- 当线程要修改一个共享变量的时候, 也会先修改工作内存中的副本, 再同步回主内存.
由于每个线程有自己的工作内存, 这些工作内存中的内容相当于同一个共享变量的 “副本”. 此时修改线程 1 的工作内存中的值, 线程2 的工作内存不一定会及时变化
1)初始情况下, 两个线程的工作内存内容一致
2) 一旦线程1 修改了 a 的值, 此时主内存不一定能及时同步. 对应的线程2 的工作内存的 a 的值也不一定能及时同步
这个时候代码就容易出现问题
也就是说 CPU 在和内存交互的时候,经常会把主内存的内容,拷贝到工作内存,然后进行操作,再写回到主内存.
这个过程就非常容易出现数据不一致的情况,这种情况在编译器开启优化的时候会特别严重.
volatile 或者 synchronized 就能够强制保证接下来的操作是操作内存:禁止指令重排序
使用 volatile 修饰变量时,根据 volatile 重排序规则表,Java 编译器在生成字节码时,会在指令序列中插入内存屏障指令来禁止特定类型的处理器重排序。
内存屏障
是一组处理器指令,它的作用是禁止指令重排序和解决内存可见性的问题。
三.wait和notify
线程之间是抢占式执行,也就意味着,充满了不确定性.我们无法确定线程的执行顺序.而通过wait和notify机制就可以来控制线程之间的执行顺序.让多个线程之间更好的相互配合
1.wait()
wait 作用:
- 使当前执行代码的线程进行等待.(把线程从就绪队列放到等待队列中)
- 释放当前的锁
- 满足一定条件时被唤醒,重新尝试获取这个锁
wait 结束等待的情况:
- 其他线程调用该对象的 notify 方法
- wait 等待时候超时(wait可以指定一个最长等待时间)
- 其他线程调用该线程的 interrupted 方法.导致 wait 抛出 InterruptedException 异常
wait 以及 notify 必须要搭配 synchronized 进行使用
2.notify()
作用: 唤醒等待的线程
- 方法notify()也要在同步方法或同步块中调用,该方法是用来通知那些可能等待该对象的对象锁的其它线程,对其发出通知notify,并使它们重新获取该对象的对象锁。
- 如果有多个线程等待,则有线程调度器随机挑选出一个呈 wait 状态的线程。(并没有 “先来后到”)
- 在执行 notify()方法后,当前线程不会马上释放该对象锁,要等到执行notify()方法的线程将程序执行完,也就是退出同步代码块之后才会释放对象锁
wait 和 notify 使用示例:
static class MyThread extends Thread{
private Object locker = null;
public MyThread(Object locker) {
this.locker = locker;
}
@Override
public void run() {
synchronized (this.locker) {
System.out.println(Thread.currentThread().getName()+": 开始等待");
try {
this.locker.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
while(true){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+": 运行中");
}
}
}
}
static class MyRunnable implements Runnable{
private Object locker = null;
public MyRunnable(Object locker) {
this.locker = locker;
}
@Override
public void run() {
synchronized (this.locker) {
System.out.println("3s后准备解锁!");
try {
Thread.sleep(3000);
System.out.println("3s 之期已至! 解除封印!");
this.locker.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
/**
* 首先使用 wait 方法让线程 t1 进入等待队列中,然后经过 3s 后,
* 执行到 线程2 的 notify 方法释放锁.当然,必须得对同一把锁进行操作.
* 这里用到了一个锁对象
* @param args
*/
public static void main(String[] args) {
Object locker = new Object();
Thread t1 = new MyThread(locker);
Thread t2 = new Thread(new MyRunnable(locker));
t1.start();
t2.start();
}
3.notifyAll()
notify方法只是唤醒某一个等待线程. 使用notifyAll方法可以一次唤醒所有的等待线程
public void run() {
synchronized (locker) {
System.out.println("notify 开始");
locker.notifyAll();
System.out.println("notify 结束");
}
}
通过这个方法,就可以将所有的被 wait 方法作用的线程给唤醒,放回到就绪队列中.不管是一个线程还是多个线程.
当然,如果单独的 notify() 就只能唤醒一个,若是多个线程被 wait() 作用的线程,则就随机唤醒一个线程.