本文共 16293 字,大约阅读时间需要 54 分钟。
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 <133; i++) { System.out.println(super.getName()+ " " + i); //System.out.println(Thread.currentThread().isDaemon()); } }}
4、线程优先级(1-10):
每个线程都有优先级,优先级的高低只和线程获得执行机会的次数多少有关,并非线程优先级越高的就一定先执行,哪个线程的先运行取决于CPU的调度。
MAX_PRIORITY=10,最高优先级
MIN_PRIORITY=1,最低优先级
NORM_PRIORITY=5,默认优先级
方法:
int getPriority() ; //返回此线程的优先级
void setPriority(int newPriority) ;//更改此线程的优先级。
每个线程都有默认优先级,主线程默认优先级为5,如果A线程创建了B线程,那么B线程和A线程具有相同优先级。
注意:不同的操作系统支持的线程优先级不同的,建议使用上述三个常量优先级就够了,不要自定义。
public class ThreadDemo { public static void main(String[] args){ //设置当前线程的优先级 Thread.currentThread().setPriority(8); //获取当前的线程的优先级级别 System.out.println(Thread.currentThread().getPriority()); // 8 PriorityThread max = new PriorityThread("高优先级"); max.setPriority(Thread.MAX_PRIORITY); PriorityThread min = new PriorityThread("低优先级"); min.setPriority(Thread.MIN_PRIORITY); min.start(); max.start(); }}class PriorityThread extends Thread{ public PriorityThread(String name){ super(name);//调用父类的构造器 } @Override public void run(){ for (int i = 0; i < 10; i++) { System.out.println(super.getName() + "-" + i); } }}
5、线程礼让:
static void yield() 方法:表示当前线程对象提示调度器自己愿意让出CPU资源,但是调度器可以自由的忽略该提示。
调用该方法之后,线程对象进入就绪状态,所以完全有可能某个线程调用了yield()之后,线程调度器又把它调度出来重新执行。
从 Java7 提供的文档上可以清楚的看出,开发中很少会使用到该方法,该方法主要用于调试或测试,它可能有助于因多线程竞争条件下的错误重现现象。
public class ThreadDemo { public static void main(String[] args){ YieldThread max = new YieldThread("高优先级"); max.setPriority(Thread.MAX_PRIORITY); YieldThread min = new YieldThread("低优先级"); min.setPriority(Thread.MIN_PRIORITY); min.start(); max.start(); }}class YieldThread extends Thread{ public YieldThread(String name){ super(name);//调用父类的构造器 } @Override public void run(){ for (int i = 0; i < 100; i++) { System.out.println(super.getName() + "-" + i); if(i ==20){ Thread.yield();//当i=20的时候,做出礼让 (效果不明显) } } }}
sleep方法和yield方法的区别:
1)都能使当前处于运行状态的线程放弃CPU,把运行的机会给其他线程.
2)sleep方法会给其他线程运行机会,但是不考虑其他线程的优先级,yield方法只会给相同优先级或者更高优先级的线程运行的机会.
3)调用sleep方法后,线程进入计时等待状态,调用yield方法后,线程进入就绪状态.
6、线程中断方法
1)interrupt()方法,作用于调用者线程
表示中断此线程(指调用该方法的Thread实例所代表的线程),但实际上只是给线程设置一个中断标志,线程仍会继续运行。
2)isInterrupted()方法,作用于调用者线程
表示测试此线程(调用者代表的线程)是否已经中断,不清除中断状态。
3)interrupted() 静态方法,作用于当前线程
表示测试当前线程是否已经中断并清除中断状态(中断标志),有中断标记的话,返回一个ture并清除中断状态,第二次再调用时中断状态已经被清除,则返回一个false。
public class InterruptDemo { public static void main(String[] args) { Thread threadDemo = new Thread(new ThreadDemo()); threadDemo.start(); // 给 threadDemo打中断标记 threadDemo.interrupt(); // 此线程:threadDemo System.out.println(threadDemo.isInterrupted()); // true System.out.println(threadDemo.isInterrupted()); // true System.out.println("线程是否存活" + threadDemo.isAlive()); // true // 当前线程:main线程 无标记 System.out.println(Thread.interrupted()); // fasle // 给当前线程打中断标记 Thread.currentThread().interrupt(); System.out.println(Thread.interrupted()); // true System.out.println(Thread.interrupted()); // fasle } private static class ThreadDemo implements Runnable{ @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + "==" + i); } } }}
interrupt()只是打标记,若想要是实现调用interrupt()方法真正的终止线程,则可以在线程的run方法中做处理即可,比如直接跳出run()方法使线程结束。
private static class ThreadDemo implements Runnable{ @Override public void run() { for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + "==" + i); if(Thread.currentThread().isInterrupted()){ return; } } } }
7、定时器和线程组
1、定时器
在JDK的java.util包中提供了Timer类,可以定时执行特定的任务.
TimerTask子类表示定时器执行的某一项任务.
常用方法:
void schedule(TimerTask task, Date firstTime, long period) ;//从指定 的时间开始 ,对指定的任务执行重复的 固定延迟执行 。
void schedule(TimerTask task, long delay) ;//在指定的延迟之后安排指定的任务执行。
void cancel();// 终止此计时器,丢弃任何当前计划的任务。
public class ThreadDemo { public static void main(String[] args){ System.out.println("begin..."); Timer timer = new Timer(); for (int i = 0; i < 5; i++) { //此处Timer必须设置为前台线程,否则看不到效果 //timer.schedule(new PrintDemo(), 3000); } System.out.println("end..."); //timer.cancel();//终止此计时器 // 只是用一次,可以用匿名内部类 timer.schedule(new TimerTask() { @Override public void run() { System.out.println(new Date().toLocaleString()); } }, 2000,1000); }}//定时器class PrintDemo extends TimerTask { @Override public void run(){ //在3秒之后打印内容5次 System.out.println("五一节快乐~"); }}
2、线程组
ThreadGroup类表示线程组,可以对一组线程进行集中管理.
用户在创建线程对象时,可以通过构造器指定其所属的线程组.
构造器:Thread(ThreadGroup group, String name) ;//分配一个新的 Thread对象。
如果A线程创建了B线程,如果没有设置B线程的分组,那么B线程加入到A线程的线程组.
一旦线程加入某个线程组,该线程就一直存在于该线程组中直到线程死亡,不能在中途修改线程的分组.
当Java程序运行时,JVM会创建名为main的线程组,在默认情况下,所有的线程都是该在线程组下.
ends ~
转载地址:http://nncgn.baihongyu.com/