java多线程详解
转自:线程间通信、等待唤醒机制、生产者消费者问题(Lock,Condition)、停止线程和守护线程、线程优先级
1 线程间通信
1.1 线程间通信
其实就是多个线程在操作同一个资源,但是操作的动作不同。
比如一个线程给一个变量赋值,而另一个线程打印这个变量。
1.2 等待唤醒机制
wait():将线程等待,释放了CPU执行权,同时将线程对象存储到线程池中。
notify():唤醒线程池中一个等待的线程,若线程池有多个等待的线程,则任意唤醒一个。
notifyAll():唤醒线程池中,所有的等待中的线程。
这三个方法都要使用在同步中,因为要对持有锁的线程进行操作。
比如,A锁上的线程被wait了,那么这个线程就进入了A锁的线程池中,只能被A锁的notify唤醒,而不能被不同锁的其他线程唤醒。
所以这三个方法要使用在同步中,因为只有同步才具有锁。
而锁可以是任意对象,这三个方法被锁调用,所以这三个方法可以被任意对象调用,所以这三个方法定义在Object类中。
wait()和sleep()的区别:
wait():可以指定等待的时间,也可以不指定时间,如果不指定时间,就只能被同一个锁的notify或notifyAll唤醒。wait时线程会释放CPU执行权,并且会释放锁。
sleep():必须指定线程休眠的时间,线程休眠即暂停执行。时间到了,线程就自动苏醒,恢复运行。sleep时线程会释放执行权,但不释放锁。
线程的停止:
1,如果run()方法中定义了循环,可以用循环结束标记,跳出循环,则线程就停止了。
2,如果线程已被冻结,读不到循环结束标记,则需要通过Thread类的interrupt方法中断线程,让线程重新获得执行的资格,从而可以读到循环结束标记,而结束线程。
3,setDaemon(true)方法将当前线程标记为守护线程,当运行的线程都是守护线程时,则Java虚拟机退出。该方法必须在启动线程前调用。
等待唤醒机制代码,实现两个线程交替执行,在控制台上交替打印两个字符串。
等待和唤醒是同一个锁r:
- //两个线程交替执行,在控制台交替打印两串字符串。
- class Res{
- String name;
- String sex;
- boolean flag = false; //等待唤醒机制
- }
- class Input implements Runnable{
- private Res r;
- Input(Res r){
- this.r = r;
- }
- public void run(){
- int x = 0;
- while(true){
- synchronized(r){ //等待和唤醒,是同一个锁。
- if(r.flag) //等待唤醒机制,true则等待,false则执行
- try{r.wait();}catch(Exception e){} //线程等待,进入线程池
- if(x == 0){
- r.name = "LuoQi";
- r.sex = "man";
- }
- else{
- r.name = "丽丽"; //赋值时,赋值了name还没赋值sex,就打印“lili----male”,加同步锁,牢记同步前提。
- r.sex = "女";
- }
- x = (x+1)%2;
- r.flag = true; //等待唤醒机制
- r.notify(); //任意唤醒线程池里的一个被等待的线程 //等待唤醒机制
- }
- }
- }
- }
- class Output implements Runnable{
- private Res r;
- Output(Res r){
- this.r = r;
- }
- public void run(){
- while(true){
- synchronized(r){ //等待和唤醒,是同一个锁。
- if(!r.flag) //等待唤醒机制,false则等待,true则执行
- try{r.wait();}catch(Exception e){} //线程等待,进入线程池
- System.out.println(r.name+"----"+r.sex);
- r.flag = false; //等待唤醒机制
- r.notify(); //唤醒Input线程 //等待唤醒机制
- }
- }
- }
- }
- class ThreadCommunication{
- public static void main(String[] args){
- Res r = new Res();
- Input in = new Input(r);
- Output out = new Output(r);
- Thread t1 = new Thread(in);
- Thread t2 = new Thread(out);
- t1.start();
- t2.start();
- }
- }
以上代码中,把两个同步代码块中的代码,封装成两个同步方法,一个更改两个字段的值,另一个打印两个字段的值。
两个同步方法写在Res类中,这样同步锁都是Res.class字节码文件,保证了等待和唤醒是同一个锁:
- //两个线程交替执行,在控制台交替打印两串字符串。
- class Res{
- String name;
- String sex;
- boolean flag = false;
- public synchronized void setRes(String name,String sex){ //同步函数
- if(this.flag) //flag为true,则线程等待进入线程池。
- try{this.wait();}catch(Exception e){}
- this.name = name; //flag为false,则线程继续执行。
- this .sex = sex;
- this.flag = true;
- this.notify(); //任意唤醒线程池中一个等待的线程
- }
- public synchronized void getRes(){
- if(!this.flag) //flag为false,则线程等待进入线程池。
- try{this.wait();}catch(Exception e){}
- System.out.println(this.name+"----"+this.sex); //flag为true则继续执行
- this.flag = false;
- this.notify(); //任意唤醒线程池中一个等待的线程
- }
- }
- class Input implements Runnable{
- private Res r;
- Input(Res r){
- this.r = r;
- }
- public void run(){
- int x = 0;
- while(true){
- if(x == 0)
- r.setRes("LuoQi","man");
- else
- r.setRes("丽丽","女");
- x = (x+1)%2;
- }
- }
- }
- class Output implements Runnable{
- private Res r;
- Output(Res r){
- this.r = r;
- }
- public void run(){
- while(true){
- r.getRes();
- }
- }
- }
- class ThreadCommunication2{
- public static void main(String[] args){
- Res r = new Res();
- Input in = new Input(r);
- Output out = new Output(r);
- Thread t1 = new Thread(in);
- Thread t2 = new Thread(out);
- t1.start();
- t2.start();
- }
- }
2 生产者消费者问题
2.1 JDK1.5以前
使用线程间通信和线程同步解决生产者消费者问题。
while循环判断标记和notifyAll():
当出现多个生产者和多个消费者时,必须用while循环判断标记,和notifyAll唤醒全部线程。
对于多个生产者和消费者,为什么要定义while判断标记?
原因:让被唤醒的线程再一次判断标记。
为什么使用notifyAll()?
因为需要唤醒对方线程,因为notify是随机唤醒一个线程,容易出现只唤醒本方线程的情况,导致程序中的所有线程都等待。
代码和注释:
- package mypkg;
- class ProducerConsumerDemo{
- public static void main(String[] args){
- Resource r = new Resource();
- Producer pro = new Producer(r);
- Consumer con = new Consumer(r);
- Thread t1 = new Thread(pro); //两个生产者线程
- Thread t2 = new Thread(pro);
- Thread t3 = new Thread(con); //两个消费者线程
- Thread t4 = new Thread(con);
- t1.start();
- t2.start();
- t3.start();
- t4.start();
- }
- }
- class Resource{
- private String name;
- private int count = 1;
- private boolean flag = false;
- public synchronized void set(String name){ //t1 t2
- while(this.flag) //while循环判断标记,让被唤醒的线程再次判断标记。标记为true则线程等待,为false则线程继续执行
- try{this.wait();} catch(Exception e){}
- this.name = name+"--"+count++;
- System.out.println(Thread.currentThread().getName()+"---生产者---"+this.name);
- this.flag = true;
- this.notifyAll(); //必须唤醒对方,索性唤醒全部。因为有可能生产者唤醒了生产者,导致有的商品被生产了但没被消费。
- }
- public synchronized void get(){ //t3 t4
- while(!this.flag)
- try{this.wait();} catch(Exception e){}
- System.out.println(Thread.currentThread().getName()+"---消费者---------"+this.name);
- this.flag = false;
- this.notifyAll();
- }
- }
- class Producer implements Runnable{
- private Resource r;
- Producer(Resource r){
- this.r = r;
- }
- public void run(){
- while(true){
- r.set("+商品+");
- }
- }
- }
- class Consumer implements Runnable{
- private Resource r;
- Consumer(Resource r){
- this.r = r;
- }
- public void run(){
- while(true){
- r.get();
- }
- }
- }
运行结果:
2.2 JDK1.5以后
JDK1.5 中提供了线程同步和线程间通信的升级解决方案,线程同步、线程间通信和等待唤醒机制都有了变化。
1,将同步Synchronized替换成显式的Lock操作。
2,将同步锁继承自Object类的wait()、notify()、notifyAll()操作,替换成了Condition对象的await()、signal()、signalAll()操作。
3,该Condition对象可以通过显式的Lock锁来创建。
显式的锁机制,以及显式的锁对象上的等待唤醒操作机制,同时把等待唤醒进行封装。
封装完后,一个锁可以对应多个Condition,等待和唤醒必须是同一个Condition对象调用。
JDK1.5之前,等待和唤醒必须是同一个锁调用;
JDK1.5之后,等待和唤醒必须是同一个Condition对象调用,而一个Lock锁可以创建多个Condition对象。
从而,可以在生产者线程中,只唤醒消费者的等待线程,即调用消费者的Condition对象的唤醒操作。
Lock接口,它的一个子类是ReentrantLock,创建对象时new一个ReentrantLock对象。
ReentrantLock类的常用方法:
newCondition():创建锁Lock的Condition对象,用来调用操作。
lock():获取锁。
unlock():释放此锁。
Condition类的常用方法:
await(): 线程进入等待状态,并抛出一个InterruptedException异常。
signal(): 唤醒一个等待线程。
signalAll(): 唤醒所有等待线程。
- import java.util.concurrent.locks.*;
- class ProducerConsumerDemo2{
- public static void main(String[] args){
- Resource r = new Resource();
- Producer pro = new Producer(r);
- Consumer con = new Consumer(r);
- Thread t1 = new Thread(pro);
- Thread t2 = new Thread(pro);
- Thread t3 = new Thread(con);
- Thread t4 = new Thread(con);
- t1.start();
- t2.start();
- t3.start();
- t4.start();
- }
- }
- class Resource{
- private String name;
- private int count = 1;
- private boolean flag = false;
- final Lock lock = new ReentrantLock(); //创建一个锁
- final Condition condition_pro = lock.newCondition(); //创建锁lock的Condition对象,用来操作生产者线程
- final Condition condition_con = lock.newCondition(); //创建锁lock的Condition对象,用来操作消费者线程
- public void set(String name) throws InterruptedException { //t1 t2
- lock.lock();
- try{
- while(this.flag)
- condition_pro.await(); //await():线程等待,会抛出一个异常
- this.name = name+"--"+count++;
- System.out.println(Thread.currentThread().getName()+"---生产者---"+this.name);
- this.flag = true;
- condition_con.signal(); //生产者中唤醒消费者
- }
- finally{
- lock.unlock(); //释放锁的动作一定要执行,所以在finally中
- }
- }
- public void get() throws InterruptedException { //t3 t4
- lock.lock();
- try{
- while(!this.flag)
- condition_con.await();
- System.out.println(Thread.currentThread().getName()+"---消费者---------"+this.name);
- this.flag = false;
- condition_pro.signal(); //消费者中唤醒生产者
- }
- finally{
- lock.unlock();
- }
- }
- }
- class Producer implements Runnable{
- private Resource r;
- Producer(Resource r){
- this.r = r;
- }
- public void run(){
- while(true){
- try{
- r.set("+商品+");
- }
- catch(InterruptedException e){}
- }
- }
- }
- class Consumer implements Runnable{
- private Resource r;
- Consumer(Resource r){
- this.r = r;
- }
- public void run(){
- while(true){
- try{
- r.get();
- }
- catch(InterruptedException e){}
- }
- }
- }
3 停止线程和守护线程
3.1 停止线程
以前可以使用stop方法来停止线程,但是已经过时,那现在如何停止线程?
只有一种方法:run方法结束。
开启多线程运行,run方法内的运行代码通常是循环结构,
只要控制住循环,就可以让run方法结束,也就是线程结束。
特殊情况:
当线程处于了冻结状态,就不会读取到标记,那么线程就不会结束。
当没有指定的方式让冻结的线程恢复到运行状态时,这是需要对冻结进行清除。
强制让现场恢复到运行状态中来,这样就可以操作标记让线程结束。
Thread类中提供该方法,interrupt()方法。
interrupt()方法是把线程从冻结状态恢复到运行状态。
3.2 守护线程
Thread类中的setDaemon方法
setDaemon(boolean on):
on如果为 true,则将该线程标记为守护线程。
守护线程,当正在运行的线程都是守护线程时,Java 虚拟机退出。该方法必须在启动线程前调用。
JVM退出,守护线程在后台执行,理解为后台线程;全部为后台线程时,由前台转为后台,JVM则退出。
代码示例:
- class StopThread implements Runnable{
- private boolean flag = true;
- public synchronized void run(){
- while(flag){
- try{
- wait();
- }
- catch(InterruptedException e){
- System.out.println(Thread.currentThread().getName()+"....Exception");
- flag = false;
- }
- System.out.println(Thread.currentThread().getName()+"....run");
- }
- }
- public void changeFlag(){
- flag = false;
- }
- }
- class StopThreadDemo{
- public static void main(String[] args){
- StopThread st = new StopThread();
- Thread t1 = new Thread(st);
- Thread t2 = new Thread(st);
- //t1.setDaemon(true); //守护线程,当正在运行的线程都是守护线程时,Java 虚拟机退出。该方法必须在启动线程前调用。
- //t2.setDaemon(true);
- t1.start();
- t2.start();
- int num = 0;
- while(true){
- if(num++ == 60){
- //st.changeFlag();
- t1.interrupt(); //中断线程,让线程从冻结状态恢复到运行状态,这样可以读到flag标记从而结束线程。
- t2.interrupt();
- break; //跳出while循环
- }
- System.out.println(Thread.currentThread().getName()+"......"+num);
- }
- System.out.println("over");
- }
- }
4 线程的join()方法
join():
当A线程执行到了B线程的join()方法时,那么A线程就会等待;等B线程执行完,A才会执行。
join()可以用来临时加入线程执行。
代码示例:
- class Demo implements Runnable{
- public void run(){
- for(int x=0;x<70;x++){
- System.out.println(Thread.currentThread().getName()+"....."+x);
- }
- }
- }
- class JoinDemo{
- public static void main(String[] args) throws Exception{
- Demo d = new Demo();
- Thread t1 = new Thread(d);
- Thread t2 = new Thread(d);
- t1.start();
- t2.start();
- t1.join(); //t1线程向主线程索要CPU执行权,主线程阻塞,释放CPU执行权,但释放后t1和t2竞争CPU执行权;
- //t1线程执行结束后,主线程继续。
- for(int x=0; x<80; x++){
- System.out.println("main...."+x);
- }
- System.out.println("over");
- }
- }
5 线程优先级和yield()方法
5.1 线程优先级
线程优先级:
优先级高的线程,争夺CPU执行权的频率就高,拿到CPU资源的可能性更大,
但并不是说优先级低的线程就不执行了。
Thread类中定义了三个优先级常量:
MAX_PRIORITY 值为10,为最高优先级;
MIN_PRIORITY 值为1,为最低优先级;
NORM_PRIORITY 值为5,默认优先级。
新建线程将继承创建它的父线程的优先级,父线程是指执行创建新线程的语句所在线程,它可能是主线程,也可能是另一个自定义线程。
一般情况下,主线程具有默认优先级,为5。
可以通过getPriority()方法获得线程的优先级,也可以通过setPriority()方法来设定优先级。
5.2 yield()方法
yield()方法:调用该方法后,可以使具有与当前线程相同优先级的线程有运行的机会。
可以临时暂停当前线程,释放CPU执行权,让相同优先级的其他线程运行。
如果没有相同优先级的线程,那么yield()方法什么也不做,当前线程继续运行。
代码示例:
- class Demo implements Runnable{
- public void run(){
- for(int x=0;x<70;x++){
- System.out.println(Thread.currentThread().getName()+"....."+x);
- Thread.yield(); //t1暂停,t2运行;t2暂停,t1运行。
- //表现为t1、t2交替执行。
- }
- }
- }
- class YieldDemo{
- public static void main(String[] args){
- Demo d = new Demo();
- Thread t1 = new Thread(d);
- Thread t2 = new Thread(d);
- t1.start();
- t2.start();
- for(int x=0; x<80; x++){
- //System.out.println("main...."+x);
- }
- System.out.println("over");
- }
- }
6 开发中什么时候使用多线程?
当某些代码需要同时被执行时,就用单独的线程进行封装。
比如三个for循环同时运行,用多线程,高效,代码示例:
- class ThreadTest{ //三个for同时运行,用多线程,高效。
- public static void main(String[] args){
- new Thread(){ //匿名内部类
- public void run(){
- for(int x=0; x<50; x++){
- System.out.println(Thread.currentThread().getName()+"....."+x);
- }
- }
- }.start();
- for(int x=0; x<50; x++){
- System.out.println(Thread.currentThread().getName()+"....."+x);
- }
- Runnable r = new Runnable(){ //匿名内部类
- public void run(){
- for(int x=0; x<50; x++){
- System.out.println(Thread.currentThread().getName()+"....."+x);
- }
- }
- };
- new Thread(r).start();
- }
- }