目录
4. wait()和notify() | notifyAll()
一,Synchronized同步锁回顾
1. 锁介绍
分类:
-
乐观锁:以其他的方式实现了线程安全,实际无锁的操作。
-
悲观锁:真正意义的锁。
在Java中每个对象或类都可以当做锁使用,这些锁称为内置锁。
Java中内置锁都是互斥锁。也就是说一个线程获取到锁,其他线程必须等待或阻塞。 如果占用锁的线程不释放锁,其他线程将一直等待下去。锁在同一时刻,只能被一个线程持有。
如果锁是作用于对象,称对象锁。如果锁作用整个类称为类锁。
2. synchronized介绍
-
synchronized是Java中的关键字。使用synchronized关键字是锁的一种实现。
-
synchronized的加锁和解锁过程不需要程序员手动控制,只要执行到synchronized作用范围会自动加锁(获取锁/持有锁),执行完成后会自动解锁(释放锁)。加锁范围中的代码出现异常,自动解锁。
-
synchronized可以保证可见性,因为每次执行到synchronized代码块时会清空线程区(工作内存|高速缓存)。
-
synchronized 会不禁用指令重排,但可以保证有序性。因为同一个时刻只有一个线程能操作。
-
synchronized 可以保证原子性,一个线程的操作一旦开始,就不会被其他线程干扰,只能当前线程执行完,其他线程才可以执行。
-
synchronized 在Java老版本中属于重量级锁(耗费系统资源比较多的锁),随着Java的不停的更新、优化,在Java8中使用起来和轻量级锁(耗费系统资源比较少的锁)已经几乎无差别了。
-
主要分为下面几种情况:
-
修饰实例方法,非静态方法(对象锁) 需要在类实例化后,再进行调用。
-
修饰静态方法(类锁)静态方法属于类级别的方法,静态方法可以类不实例化就使用。
-
修饰代码块(对象锁、类锁)。
-
3. 修饰实例方法
锁类型:使用synchronized修饰实例方法时为对象锁。
锁范围:锁的范围是加锁的方法。
锁生效:必须为同一个对象调用该方法该锁才有作用。
4. 修饰静态方法
锁类型:使用synchronized修饰静态方法时为类锁。
锁范围:锁的范围是加锁的方法。
锁生效:该类所有的对象调用加锁方法,锁都生效 。
5. 修饰代码块
语法:
synchronized(锁){
// 内容
}
锁代码块是非常重要的地方。添加锁的类型是Object类型。
运行过程:
多线程执行时,每个线程执行到这个代码块时首先会判断是否有其他线程持有这个锁,如果没有,执行synchronized代码块。如果已经有其他线程持有锁,必须等待线程释放锁。当一个线程执行完成synchronized代码块时会自动释放所持有的锁。
5.1 锁为固定值
当锁为固定值时,每个线程执行到synchronized代码块时都会判断这个锁是否被其他线程持有,哪个线程抢到先执行哪个线程。当抢到的线程执行完synchronized代码块后,会释放锁,其他线程竞争,抢锁,抢到的持有锁,其他没抢到的继续等待。
由于值固定不变,所有的对象调用加锁的代码块,都会争夺锁资源,属于类锁。
5.2 锁为不同内容
每个线程中的synchronized锁不相同时,相当于没有加锁。
因为没有需要竞争锁的线程,线程执行到synchronized时,直接获取锁,进入到代码块。
5.3 锁为this
当锁为this时,需要看线程中是否为同一个对象调用的包含synchronized所在的方法。这种写法也是比较常见的写法。
5.3.1 同一个对象调用加锁方法时:
如果是同一个对象调用synchronized所在方法时,this代表的都是一个对象。this就相当于固定值。所以可以保证结果正确性,属于对象锁。
5.3.2 不同对象调用加锁方法时:
如果不是同一个对象调用synchronized所在方法时,this所代表的对象就不同。相当于锁为不同内容时,锁失效。
5.4 锁为class
锁为Class时,是一个标准的类锁,所有的对象调用加锁的代码块都生效。
6. 对象锁和类锁
当synchronized修饰静态方法或代码块参数为Class时或代码块参数为固定值,锁为类锁,作用整个类。同一个类使用,锁生效。
当synchronized修饰实例方法或代码块参数为this时,为对象锁,只对当前对象有效。
体现在:
多个对象使用时,锁生效,使用类锁。
同一对象使用时,所生效,使用对象锁。
7. 什么是可重入锁
某个线程已经获得了某个锁,允许再次获得锁,就是可重入锁。如果不允许再次获得锁就称为不可重入锁。
synchronized为可重入锁。但可重入锁不仅仅只有synchronized。后面还会学习ReentrantLock也是可重入锁。
7.1 代码演示
test1()、test2()中都使用的同一把锁。在执行test1()时没有人持有”锁”,所以进入到test1()中的synchronized,输出test1,调用test2()方法,因为是同一个线程,且synchronized是可重入锁,所以允许继续执行,就会进入到test2(),执行synchronized,输出test2。
反之,如果synchronized是不可重入锁的话,在执行到test2的synchronized时会阻塞。因为test1()中synchronized已经持有锁,且没有释放锁。
public class Test03 {
public static void main(String[] args) {
Test03 test03 = new Test03();
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
test03.test1();
}
}).start();
}
}
public void test1(){
synchronized (this){
System.out.println("test1执行");
test2();
}
}
public void test2(){
synchronized (this){
System.out.println("test2执行");
}
}
}
二,生命周期回顾
线程生命周期从新建到死亡共包含五种状态:
新建状态、就绪状态、运行状态、阻塞状态、死亡状态
2.1 新建状态
当实例化Thread对象后,线程就处于新建状态. 这时线程并没有执行。 (也就是new对象之后)
2.2 就绪状态
只要在代码中启动了线程,就会从新建状态,变为就绪状态。(使用start()方法启动线程)
2.3 运行状态
运行状态就是开始执行线程的功能。具体就是执行run()方法
在代码执行过程中分为三种情况:
1. 如果碰到sleep() / wait() / join()等方法会让线程切换为阻塞状态。
2. 如果调用yield()方法或失去CPU执行权限会切换为就绪状态。
3. 如果run()方法成功执行完成,或出现问题或被停止(中断)会切换为死亡状态。
2.4 阻塞状态
阻塞状态时,线程停止执行。让出CPU资源。
处于阻塞状态的线程需要根据情况进行判断是否转换为就绪状态:
1. 如果是因为sleep()变为阻塞,则休眠时间结束自动切换为就绪状态。
2. 如果是因为wait()变为阻塞状态,需要调用notify()或notifyAll()手动切换为就绪状态。
3. 如果因为join()变为阻塞状态,等到join线程执行完成,自动切换为就绪状态。
4. (已过时)如果是因为suspend()暂停的线程,需要通过resume()激活线程。
2.5 死亡状态
死亡状态即线程执行结束。
三,线程中相关方法回顾
1. stop()介绍(已过时)
1.1 stop()介绍(已过时)
stop()可以停止一个线程。让线程处于死亡状态,stop()已经过时
1.2 stop()弃用的原因
stop()本身就是不安全的,强制停止一个线程,可能导致破坏线程内容。导致错误的结果。同时程序还没有任务异常。
推荐使用interrupt()停止一个长时间wait的线程。
stop()太绝对了,什么情况下都能停,并没有任何的提示信息,可能导致混乱结果。
2. interrupt()介绍
interrupt()作为stop()的替代方法。可以实现中断线程,并结束该线程。
interrupt()只能中断当前线程状态带有InterruptedException异常的线程,当程序执行过程中,如果被强制中断会出现Interrupted异常。
interrupt() 负责打断处于阻塞状态的线程。防止出现死锁或长时间wait(等待)。
2.1 停止并结束阻塞状态的线程
-
Thread类中的sleep()方法
-
代码实现
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("run方法开始执行");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("run方法执行结束");
}
});
thread.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
thread.interrupt();
}
2.2 停止并结束运行状态的线程
运行状态没有抛出InterruptException 所以不能中断。
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (true){
System.out.println("run()开始执行");
System.out.println("run()执行结束");
}
}
});
thread.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
thread.interrupt();
}
3. suspend()和resume()
3.1 suspend()介绍(已过时)
suspend()可以挂起、暂停线程,让线程处于阻塞状态,是一个实例方法,已过时。
挂起时,不会释放锁
3.2 resume()介绍(已过时)
resume()可以让suspend()的线程唤醒,变成就绪状态,已过时。
3.3 被弃用的原因
死锁:当一个线程持有锁,因为各种原因,不释放锁。其他线程又想拿到这个锁,但拿不到,这时这个锁就称为死锁。
由于这两个已经是过时方法,容易产生死锁,目前已经很少使用了。
官方解释:
已弃用
此方法已被弃用,因为它本质上容易死锁。 如果目标线程在挂起时保护关键系统资源的监视器上持有锁,则在目标线程恢复之前,没有线程可以访问该资源。 如果将恢复目标线程的线程在调用resume之前尝试锁定此监视器,则会导致死锁。 这种死锁通常表现为“冻结”进程。
解释说明:
如果线程A持有锁(假设锁叫做L),对线程A做了suspend,让线程A挂起。在线程A没有resume之前,线程B无论如何也是无法获得锁的,也就出现了死锁。因为suspend时没有释放锁。
3.5 代码演示:死锁
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
test(Thread.currentThread().getName());
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
test(Thread.currentThread().getName());
}
});
thread.start();
Thread.sleep(500);//主线程休眠1秒
//线程挂起 -> 线程阻塞状态
thread.suspend();
Thread.sleep(500);
thread1.start();
thread.interrupt();
//thread.interrupt(); suspend()方法没有声明InterruptedException,不能被中断
}
public synchronized static void test(String name){
for (int i = 0; i < 100; i++) {
try {
System.out.println(i + ":" + name);
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
四、线程通信回顾
1. 什么是线程通信
需要多个线程配合完成一件事情,如何让多个线程能够合理的切换就是线程通信需要考虑的问题,重点在于配合。
2. 生产者消费者模式
生产者和消费者模式为最经典的线程通信案例:
1. 需求:
1. 商品具有库存数。
2. 如果商品的库存满了,可以让用户进行购买/消费这个商品。
3. 如果库存为0,商品需要进货,补充库存,库存满了之后才能继续消费。
2. 实现思路:
1. 提供成员变量,代表库存数。
2. 如果库存为10,执行消费的线程减少该变量的值。
3. 如果库存为0. 执行生产的线程增加该变量的值。
3. 代码实现:
/* 库存 */
static int totalCount = 0;
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (true){
synchronized (Test07.class) {
System.out.println("---生产者---争抢锁");
if (totalCount == 0){
System.out.println("库存不足开始生产");
for (int i = 0; i < 10; i++) {
try {
totalCount++;
System.out.println("生产了"+ totalCount +"个商品");
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
while (true){
synchronized (Test07.class) {
System.out.println("===消费者===争抢锁");
if (totalCount == 10){
System.out.println("库存充足开始消费");
for (int i = 0; i < 10; i++) {
try {
totalCount--;
System.out.println("消费了"+ (10-totalCount) +"个商品");
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
}
});
thread.start();
thread1.start();
}
该方式的缺点:
-
因为两个线程一直处于运行状态和就绪状态。
-
两个线程一直在运行和就绪状态之间切换。 抢到锁的就可以执行,而没有抢到锁的线程一直在抢锁,所以对系统性能损耗较大,不推荐使用这种方式。
3. 线程通信的几种方式
-
wait()和notify() | notifyAll() 方式
-
join()方式
-
Condition 方式
-
...
4. wait()和notify() | notifyAll()
4.1 介绍
wait() 是Object中的方法。调用wait()后会让线程从运行状态变为阻塞状态。
在Object类中提供了wait()的重载方法 。
wait()方法会让线程变为阻塞,阻塞的同时会释放锁。所以wait()必须要求被等待的线程持有锁,调用wait()后会把锁释放,其他线程竞争获取锁。当其他线程竞争获取到锁以后,如果达到某个条件后可以通过notify()唤醒,如果有多个wait的线程,系统判断唤醒其中一个。如果多个处于wait的线程可以使用notifyAll全部唤醒。唤醒后线程处于就绪状态。
需要注意的是:一个线程唤醒其他线程时,要求当前线程必须持有锁
最简易结论:
1. 使用wait()和notify() | notifyAll()要求必须有锁。
2. wait()、notify()、notifyAll() 都是放入锁的代码中。
3. wait()和notify() | notifyAll() 配合使用。
4.2 代码演示
这种实现方式比while轮询方式优点在于,当执行完自己任务后,当前线程处于阻塞状态,在没有notify之前,一直是阻塞状态,不会去竞争锁。
当其他线程唤醒了wait线程时会立即让自己wait,整体没有过多性能损耗。
static int a = 0;
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (Test08.class){
while (true){
a++;
System.out.println("生产了"+a+"个产品");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (a == 10){
try {
Test08.class.notify();
Test08.class.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (Test08.class){
if (a == 0){
try {
Test08.class.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
while (true){
try {
a--;
System.out.println("消费了"+(10-a)+"个产品");
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (a == 0){
try {
Test08.class.notify();
Test08.class.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
}
});
thread.start();
thread1.start();
}
4.3 wait()和sleep()区别(常见面试题)
1,属性类不同
wait(long) 是Object中方法
sleep(long)是Thread的方法
2,唤醒机制不同
wait() 没有设置最大时间情况下,必须等待notify() | notifyAll()
sleep()是到指定时间自动唤醒
3,锁机制不同
wait(long)释放锁
sleep(long)只是让线程休眠,不会释放锁
4,使用位置不同
wait()必须持有对象锁
sleep()可以使用在任意地方
5,方法类型不同
wait()是实例方法
sleep()是静态方法
5. join()
5.1 介绍
join() 把线程加入到另一个线程中。在哪个线程内调用join(),就会把对应的线程加入到当前线程中。
join()后,会让当前线程挂起,变成阻塞状态,直到新加入的线程执行完成。当前线程才会继续执行。
5.2 需求
有两个子线程 t1,t2.
t1,t2 都进行10次for循环. 当t2线程循环到第三次时,执行t1的循环,t1循环结束后,再继续执行t2的循环
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
System.out.println(Thread.currentThread().getName() + "=>>" + i);
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
System.out.println(Thread.currentThread().getName() + "=>>" + i);
Thread.sleep(500);
if (i == 3){
thread.join();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
});
thread.setName("thread01");
thread1.setName("thread02");
thread.start();
thread1.start();
}