1、线程通信 -- 使用 synchronized 与 等待和唤醒机制
线程通信:不同的线程执行不同的任务,如果这些任务有某种关系,线程之间必须能够通信、协调完成工作.
经典的生产者和消费者案例(Producer/Consumer):
public class ThreadDemo {
public static void main(String[] args) {
// 创建生产者和消费者共同的资源对象
ShareResource shareResource = new ShareResource();
// 启动生产者线程
new Thread(new Producer(shareResource)).start();
// 启动消费者线程
new Thread(new Consumer(shareResource)).start();
}
}
// 共享资源--水果
class ShareResource{
private String name;
private String colour;
/**
* 生产者向共享资源对象中存储数据
* @param name 存储的名称
* @param colour 存储的颜色
*/
public void push(String name, String colour){
this.name = name;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.colour = colour;
}
/**
* 消费者从共享资源对象中取出数据
*/
public void popup(){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.name + "--" + this.colour);
}
}
// 生产者
class Producer implements Runnable{
// 共享资源对象
private ShareResource shareResource = null;
public Producer(ShareResource shareResource){
this.shareResource = shareResource;
}
@Override
public void run() {
for (int i = 0; i < 50; i++) {
if(i % 2 == 0){
shareResource.push("苹果","红色");
}else{
shareResource.push("柚子","橘黄色");
}
}
}
}
// 消费者
class Consumer implements Runnable{
// 共享资源对象
private ShareResource shareResource = null;
public Consumer(ShareResource shareResource){
this.shareResource = shareResource;
}
@Override
public void run() {
for (int i = 0; i < 50; i++) {
shareResource.popup();
}
}
}
分析生产者和消费者案例存在的问题:
建议在生产水果的名称和颜色之间以及在消费打印之前使用Thread.sleep(10); 使效果更明显.
问题1:出现名称与颜色紊乱的情况.
解决方案:只要保证在生产水果的名称和颜色的过程保持同步,中间不能被消费者线程进来取走数据.
可以使用同步代码块/同步方法/Lock机制来保持同步性.
// 共享资源--水果
class ShareResource{
private String name;
private String colour;
/**
* 生产者向共享资源对象中存储数据
* @param name 存储的名称
* @param colour 存储的颜色
*/
synchronized public void push(String name, String colour){
this.name = name;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.colour = colour;
}
/**
* 消费者从共享资源对象中取出数据
*/
synchronized public void popup(){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.name + "--" + this.colour);
}
}
问题2:应该出现生产一个数据,消费一个数据,交替出现
解决方案:得使用 等待和唤醒 机制.
// 共享资源--水果
class ShareResource{
private String name;
private String colour;
private boolean isEmpty = true; // 表示共享资源对象是否为空的状态
/**
* 生产者向共享资源对象中存储数据
* @param name 存储的名称
* @param colour 存储的颜色
*/
synchronized public void push(String name, String colour){
try {
while(!isEmpty){ // 当前对象为不空时等待消费者来获取
// 使用同步锁对象来调用,表示当前线程释放同步锁,进入等待池,只能被其他线程所唤醒
this.wait();
}
this.name = name;
Thread.sleep(10);
this.colour = colour;
isEmpty = false; // 设置共享资源中数据为空
this.notify(); // 唤醒一个线程(消费者),多个使用notifyAll()
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 消费者从共享资源对象中取出数据
*/
synchronized public void popup(){
try {
while(isEmpty){ // 当前对象为空时等待生产者来生产
// 使用同步锁对象来调用,表示当前线程释放同步锁,进入等待池,只能被其他线程所唤醒
this.wait();
}
Thread.sleep(10);
System.out.println(this.name + "--" + this.colour);
isEmpty = true; // 设置共享资源中数据为空
this.notify(); // 唤醒一个线程(生产者),多个使用notifyAll()
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
java.lang.Object 类提供了两类(等待和唤醒)用于线程通信的方法.
wait():执行该方法的线程对象释放同步锁,JVM把该线程存放到等待池中,等待其他的线程唤醒该线程.
notify():执行该方法的线程唤醒在等待池中等待的任意一个线程,把线程转到锁池中等待.
notifyAll():执行该方法的线程唤醒在等待池中等待的所有的线程,把线程转到锁池中等待.
注意:上述方法只能被同步监听锁对象来调,否则报错 IllegalMonitorStateException异常.
同步锁池:
同步锁一般都选择多个线程共同的资源对象.
当前生产者在生产数据时(先拥有同步锁),其他线程就在锁池中等待获取锁.
当线程执行完同步代码块时,就会释放同步锁,其他线程开始抢锁的使用权.
多个线程只有共享使用相同的一个对象时,多线程之间才有互斥效果,把这个用来做互斥的对象称之为 同步监听对象/同步锁.
同步锁对象可以选择任意类型的对象都可以,只需要保证多个线程使用的是相同锁对象即可.
因为,只有同步监听锁对象才能调用 wait和 notify方法,所以,wait和 notify方法应该存在于Object类中,而不是Thread类中.
2、线程通信 -- 使用Lock机制和Condition接口
wait和notify方法,只能被同步监听锁对象来调用,否则报错 IllegalMonitorStateException异常.
而Lock机制根本就没有同步锁,也就没有自动获取锁和自动释放锁的概念。
因为没有同步锁,所以Lock机制不能调用wait和notify方法.
解决方案:Java5中提供了Lock机制的同时也提供了处理Lock机制的通信控制的Condition接口.
从Java5开始,可以:
1)使用Lock机制取代 synchronized代码块和 synchronized方法.
2)使用Condition接口对象的await,signal,signalAll方法取代Object类中的wait,notify,notifyAll方法.
Condition接口提供一个使用实例
public class ThreadDemo {
public static void main(String[] args) {
// 创建生产者和消费者共同的资源对象
ShareResource shareResource = new ShareResource();
// 启动生产者线程
new Thread(new Producer(shareResource)).start();
// 启动消费者线程
new Thread(new Consumer(shareResource)).start();
}
}
// 共享资源--水果
class ShareResource{
private String name;
private String colour;
private boolean isEmpty = true; // 表示共享资源对象是否为空的状态
private final Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
/**
* 生产者向共享资源对象中存储数据
* @param name 存储的名称
* @param colour 存储的颜色
*/
public void push(String name, String colour){
lock.lock(); // 获取锁
try {
while(!isEmpty){ // 当前对象为不空时等待消费者来获取
condition.await();
}
this.name = name;
Thread.sleep(10);
this.colour = colour;
isEmpty = false; // 设置共享资源中数据为空
condition.signal(); // 唤醒一个线程(消费者),多个使用signalAll()
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock(); // 释放锁
}
}
/**
* 消费者从共享资源对象中取出数据
*/
public void popup(){
lock.lock(); // 获取锁
try {
while(isEmpty){ // 当前对象为空时等待生产者来生产
condition.await();
}
Thread.sleep(10);
System.out.println(this.name + "--" + this.colour);
isEmpty = true; // 设置共享资源中数据为空
condition.signal(); // 唤醒一个线程(消费者),多个使用signalAll()
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock(); // 释放锁
}
}
}
// 生产者
class Producer implements Runnable{
// 共享资源对象
private ShareResource shareResource = null;
public Producer(ShareResource shareResource){
this.shareResource = shareResource;
}
@Override
public void run() {
for (int i = 0; i < 50; i++) {
if(i % 2 == 0){
shareResource.push("苹果","红色");
}else{
shareResource.push("柚子","橘黄色");
}
}
}
}
// 消费者
class Consumer implements Runnable{
// 共享资源对象
private ShareResource shareResource = null;
public Consumer(ShareResource shareResource){
this.shareResource = shareResource;
}
@Override
public void run() {
for (int i = 0; i < 50; i++) {
shareResource.popup();
}
}
}
二、线程的生命周期
生命周期:一个事物从出生的那一刻开始到最终死亡中间的整个过程。线程也是有生命周期的,也存在不同的状态的,状态相互之间的可转换。如下图:
线程对象的状态存放在Thread类的内部类(State)中:
注意:Thread.State类其实是一个枚举类,因为线程对象的状态是固定的,只有6种,此时使用枚举来表示是最恰当的。
有人又把阻塞状态、等待状态和计时等待状态合称为阻塞状态。如下图:
1、新建状态(new)
使用new创建一个线程对象,仅仅在堆中分配内存空间,在调用start方法之前;新建状态下,线程压根就没有启动,仅仅只是存在一个线程对象而已。
Thread t = new Thread(); //此时t就属于新建状态
当新建状态下的线程对象调用了start方法,此时从新建状态进入可运行状态.
线程对象的start方法只能调用一次,否则报错:IllegalThreadStateException异常。
2、可运行状态(runnable)
分成两种状态:ready和running。分别表示就绪状态和运行状态。
就绪状态:线程对象调用start方法之后,等待JVM的调度(此时该线程并没有运行)。
运行状态:线程对象获得JVM调度,如果存在多个CPU,那么允许多个线程并行运行。
3、阻塞状态(blocked)
正在运行的线程因为某些原因放弃CPU,暂时停止运行,就会进入阻塞状态。此时JVM不会给线程分配CPU,直到线程重新进入就绪状态,才有机会转到运行状态.
阻塞状态只能先进入就绪状态,而不能直接进入运行状态.
阻塞状态的两种情况:
1)当A线程处于运行过程时,试图获取同步锁时,却被B线程获取,此时JVM把当前A线程存到对象的锁池中,A线程进入阻塞状态.
2)当线程处于运行过程时,发出了IO请求时,此时进入阻塞状态.
4、等待状态(waiting)(等待状态只能被其他线程唤醒):此时使用的无参数的wait方法,
当线程处于运行过程时,调用了wait()方法,此时JVM把当前线程存在对象等待池中.
5、计时等待状态(timed waiting)(使用了带参数的wait方法或者sleep方法)
1)当线程处于运行过程时,调用了wait(long time)方法,此时JVM把当前线程存在对象等待池中.
2)当前线程执行了sleep(long time)方法.
6、终止状态(terminated):通常称为死亡状态,表示线程终止.
1)正常执行完run方法而退出(正常死亡).
2)遇到异常而退出(出现异常之后,程序就会中断)(意外死亡).
线程一旦终止,就不能再重启启动,否则报错:IllegalThreadStateException异常。
三、线程的控制操作
1、线程休眠:让执行的线程暂停一段时间,进入计时等待状态。
方法:static void sleep(long millis)
调用sleep后,当前线程放弃CPU,在指定时间段之内,sleep所在线程不会获得执行的机会。此状态下的线程不会释放同步锁/同步监听器。该方法更多的用于模拟网络延迟,让多线程并发访问同一个资源的错误效果更明显,在开发中也会故意使用该方法。
线程的sleep方法应该写在线程的run()方法里,sleep()又是静态方法,所以最好的调用方法就是 Thread.sleep()。
注意:sleep方法只能让当前线程睡眠。调用某一个线程类的对象t.sleep(),睡眠的不是t,而是当前线程。
public static void main(String[] args) throws InterruptedException{
for (int i = 10; i >0; i--) {
System.out.println("剩余"+ i + "秒");
Thread.sleep(1000);
}
System.out.println("Boom....");
}
java.util.concurrent.TimeUnit也可以控制线程睡眠:
TimeUnit.SECONDS.sleep(1); TimeUnit.MINUTES.sleep(1); TimeUnit.HOURS.sleep(1); TimeUnit.DAYS.sleep(1);
2、联合线程:
方法:void join() :表示一个线程等待另一个线程(这个join的线程)完成后才执行。
join方法被调用之后,线程对象处于阻塞状态。
主要作用就是同步,它可以使得线程之间的并发执行变为串行执行。有人也把这种方式称为联合线程,就是说把当前线程和当前线程所在的线程联合成一个线程。
注意:join方法必须在线程start方法调用之后调用才有意义。如果一个线程都没有start,那它也就无法同步了。
比如:在A线程中调用了B线程的join()方法时,表示只有当B线程执行完毕时,A线程才能继续执行。
join方法中可以传入参数,如果A线程中调用B线程的join(10),则表示A线程会等待B线程执行10毫秒,10毫秒过后,A、B线程并发执行。注意:jdk规定,join(0)的意思不是A线程等待B线程0秒,而是A线程等待B线程无限时间,直到B线程执行完毕,即join(0)等价于join()。
public class ThreadDemo {
public static void main(String[] args) throws InterruptedException{
System.out.println("begin....");
JoinThread joinThread = new JoinThread();
for (int i = 0; i < 15; i++) {
System.out.println("main " + i);
if (i ==5 ) {
joinThread.start();//启动join线程
}
if (i == 10 ) {
joinThread.join(); //强制运行该线程,直到结束后运行另一个线程
}
}
System.out.println("end...");
}
}
//联合线程
class JoinThread extends Thread {
public void run() {
for (int i = 0; i < 50; i++) {
System.out.println("Join" + i);
}
}
}
3、后台线程:
在后台运行的线程,其目的是为其他线程提供服务,也称为“守护线程"。JVM的垃圾回收线程就是典型的后台线程。
特点:若所有的前台线程都死亡,后台线程自动死亡。反过来,如果后台线程先执行完,前台线程会不停止。
方法:
boolean isDaemon() ;//测试这个线程是否是守护线程。
void setDaemon(boolean on) ;//将此线程标记为 daemon线程或用户线程。
前台线程创建的线程默认是前台线程,可以通过setDaemon方法设置为后台线程,并且当且仅当后台线程创建的新线程时,新线程是后台线程。
设置后台线程:thread.setDaemon(true);该方法必须在start方法调用前,否则报错:IllegalThreadStateException异常。
public class ThreadDemo {
public static void main(String[] args){
//判断当前线程是否是守护线程
System.out.println(Thread.currentThread().isDaemon()); // false
for (int i = 0; i < 10; i++) {
System.out.println("main " +i);
if(i == 5){
DaemomThread dt =new DaemomThread();
dt.setDaemon(true);//设置为后台线程,并且在调用start之前设置
dt.start();
}
//当前台线程结束之后.后台线程也会相应的自动结束
}
}
}
//后台线程
class DaemomThread extends Thread{
public void run(){
for (int i = 0; i
关注
打赏
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?