Java多线程(一)

2019-10-03  本文已影响0人  一只弱狗

Java多线程(一)

1. 并发与并行

并行:指两个或多个事件在同一时刻发生(同时发生)。

并发:指两个或多个事件在同一个时间段内发生。

多线程.png

2. 进程、线程

进程

线程

多线程

多个线程并发执行。

3. 线程创建

Java中线程有四种创建方式:

3.1. 继承Thread类

第一步:创建自定义线程类

package cn.edu.nwafu;
import java.util.Date;
/**
 * @author shensr
 * @version V1.0
 * @description: 继承Thread类 创建线程
 * @create 2019/9/14
 **/
public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i <10 ; i++) {
            //getTime() 获取时间的毫秒数
            System.out.println("myThread执行的时间:"+new Date().getTime());
        }
    }
}

第二步:创建测试类

package cn.edu.nwafu;


import java.util.Date;

/**
 * @author shensr
 * @version V1.0
 * @description: 测试类
 * @create 2019/9/14
 **/


public class MyThreadTest {
    public static void main(String[] args) {
        //1.创建自定义线程类实例
        MyThread myThread = new MyThread();
        //2.启动线程
        myThread.start();
        //3.在main主线程中打印信息
        for (int i = 0; i < 10; i++) {
            System.out.println("主线程执行时间:" + new Date().getTime());
        }
    }

}

执行结果如下(不唯一)

myThread执行的时间:1568444953164
主线程执行时间:1568444953164
myThread执行的时间:1568444953165
主线程执行时间:1568444953165
myThread执行的时间:1568444953165
myThread执行的时间:1568444953165
主线程执行时间:1568444953165
myThread执行的时间:1568444953165
主线程执行时间:1568444953165
myThread执行的时间:1568444953165
主线程执行时间:1568444953165
myThread执行的时间:1568444953165
主线程执行时间:1568444953165
myThread执行的时间:1568444953165
主线程执行时间:1568444953165
myThread执行的时间:1568444953165
主线程执行时间:1568444953165
myThread执行的时间:1568444953166
主线程执行时间:1568444953166
主线程执行时间:1568444953166

3.2 实现Runnable接口

第一步:创建自定义线程类

package cn.edu.nwafu;

import java.util.Date;

/**
 * @author shensr
 * @version V1.0
 * @description: 自定义类 实现Runnable接口
 * @create 2019/9/14
 **/

public class MyRunnable implements  Runnable {
    public void run() {
        for (int i = 0; i <5 ; i++) {
            System.out.println(Thread.currentThread().getName()+"执行时间:"+new Date().getTime()+"执行次数:"+i);
        }
    }

}

第二步:创建测试类

package cn.edu.nwafu;


import java.util.Date;

/**
 * @author shensr
 * @version V1.0
 * @description: 测试 MyRunnable类
 * @create 2019/9/14
 **/


public class MyRunnableTest {

    public static void main(String[] args) {
        //一、实现Runnable接口
        //1.通过Thread类执行Runnable类
        Thread thread = new Thread(new MyRunnable());
        //2.启动线程
        thread.setName("MyRunnable");
        thread.start();
        //3.在main主线程中打印信息
        Thread.currentThread().setName("主线程");
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName()+"执行时间:" + new Date().getTime()+"执行次数:"+i);
        }
    }
}

执行结果如下(不唯一)

MyRunnable执行时间:1568448360595执行次数:0
主线程执行时间:1568448360595执行次数:0
主线程执行时间:1568448360596执行次数:1
主线程执行时间:1568448360596执行次数:2
MyRunnable执行时间:1568448360595执行次数:1
MyRunnable执行时间:1568448360596执行次数:2
主线程执行时间:1568448360596执行次数:3
MyRunnable执行时间:1568448360596执行次数:3
MyRunnable执行时间:1568448360596执行次数:4
主线程执行时间:1568448360596执行次数:4

3.3 实现Callable接口

Callable需要使用FutureTask类帮助执行,FutureTask类结构如下:

FutureTask.png
Future接口:

第一步:创建自定义线程类

package cn.edu.nwafu;

import java.util.Date;
import java.util.concurrent.Callable;

/**
 * @author shensr
 * @version V1.0
 * @description:
 * @create 2019/9/14
 **/

public class MyCallable implements Callable {
    @Override
    public Object call() throws Exception {
        for (int i = 0; i <5 ; i++) {
            System.out.println(Thread.currentThread().getName()+"执行时间:"+new Date().getTime()+"执行次数:"+i);
        }
        return "MyCallable执行完成!";
    }
}

第二步:创建测试类

package cn.edu.nwafu;

import java.util.Date;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * @author shensr
 * @version V1.0
 * @description:
 * @create 2019/9/14
 **/


public class MyCallableTest {

    public static void main(String[] args) {
        //3、实现Runnable接口
        //3.1.创建FutureTask实例,创建MyCallable实例
        FutureTask task = new FutureTask( new MyCallable());
        //3.2. 创建Thread实例,执行创建FutureTask
        Thread thread = new Thread(task,"MyRunnable");
        thread.start();
        //3.3.在main主线程中打印信息
        Thread.currentThread().setName("主线程");
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName()+"执行时间:" + new Date().getTime()+"执行次数:"+i);
        }

        //3.4. 获取并打印MyCallable返回结果
        try {
            System.out.println("MyCallable返回结果:"+task.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

执行结果如下(不唯一)

主线程执行时间:1568449491985执行次数:0
MyRunnable执行时间:1568449491985执行次数:0
主线程执行时间:1568449491985执行次数:1
MyRunnable执行时间:1568449491985执行次数:1
主线程执行时间:1568449491985执行次数:2
MyRunnable执行时间:1568449491985执行次数:2
主线程执行时间:1568449491985执行次数:3
MyRunnable执行时间:1568449491986执行次数:3
主线程执行时间:1568449491986执行次数:4
MyRunnable执行时间:1568449491986执行次数:4
MyCallable返回结果:MyCallable执行完成!

3.4 线程池-Executor

线程池线类关系图

Executor.png

Executor接口:

声明了execute(Runnable runnable)方法,执行任务代码

ExecutorService接口:

继承Executor接口,声明方法:submit、invokeAll、invokeAny以及shutDown等

AbstractExecutorService抽象类:

实现ExecutorService接口,基本实现ExecutorService中声明的所有方法

ScheduledExecutorService接口:

继承ExecutorService接口,声明定时执行任务方法

ThreadPoolExecutor类:

继承类AbstractExecutorService,实现execute、submit、shutdown、shutdownNow方法

ScheduledThreadPoolExecutor类:

继承ThreadPoolExecutor类,实现ScheduledExecutorService接口并实现其中的方法

Executors类:

提供快速创建线程池的方法

第一步:创建自定义类实现Runnable接口

package cn.edu.nwafu;

import java.util.Date;

/**
 * @author shensr
 * @version V1.0
 * @description: 自定义类 实现Runnable接口
 * @create 2019/9/14
 **/

public class MyRunnable implements  Runnable {
    public void run() {
        for (int i = 0; i <5 ; i++) {
            System.out.println(Thread.currentThread().getName()+"执行时间:"+new Date().getTime()+"执行次数:"+i);
        }
    }

}

第二步:创建测试类

package cn.edu.nwafu;

import java.util.Date;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author shensr
 * @version V1.0
 * @description: 使用线程池创建线程
 * @create 2019/9/14
 **/

public class ThreadExecutor {
    public static void main(String[] args) {
        //4. 使用线程池创建线程
        //4.1 使用Executors获取线程池对象
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        //4.2 通过线程池对象获取线程并执行MyRunnable
        executorService.execute(new MyRunnable());
        //4.3 主线程打印信息
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程执行时间:" + new Date().getTime());
        }
    }

}

4. 小结

4.1 实现接口和继承Thread类比较

4.2 Runnable和Callable接口比较

相同点:

不同点:

注意点:

5. 线程生命周期

线程生命周期.png

5.1. 新建

5.2. 就绪

5.3. 运行

5.4. 阻塞

当发生如下情况时,线程将会进入阻塞状态

5.5. 死亡

线程会以如下3种方式结束,结束后就处于死亡状态:

6. 线程安全问题

6.1. 什么是线程安全

如果有多个线程同时运行同一个实现了Runnable接口的类,程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的;反之,则是线程不安全的。

6.2. 问题演示

为了演示线程安全问题,我们采用多线程模拟多个窗口同时售卖《哪吒之魔童降世》电影票。

6.2.1. 第一步:创建售票线程类

package cn.edu.nwafu.safe;

public class Ticket implements Runnable {
    private int ticktNum = 100;
    
    public void run() {
        while(true){
            if(ticktNum > 0){
                //1.模拟出票时间
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //2.打印进程号和票号,票数减1
                String name = Thread.currentThread().getName();
                System.out.println("线程"+name+"售票:"+ticktNum--);
            }
        }
    }
}

6.2.2. 第二步:创建测试类

package cn.edu.nwafu.safe;

import cn.edu.nwafu.safe.Ticket;

public class TicketDemo {
    public static void main(String[] args){
        Ticket ticket = new Ticket();
        Thread thread1 = new Thread(ticket, "窗口1");
        Thread thread2 = new Thread(ticket, "窗口2");
        Thread thread3 = new Thread(ticket, "窗口3");
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

6.3. 问题分析

线程安全问题都是由全局变量静态变量引起的。

若每个线程对全局变量、静态变量只读,不写,一般来说,这个变量是线程安全的;

若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。

综上所述,线程安全问题根本原因:

6.4. 问题解决-线程同步

要解决以上线程问题,只要在某个线程修改共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现象。

为了保证每个线程都能正常执行共享资源操作,Java引入了7种线程同步机制。

  1.     同步代码块(synchronized)
    
  2.     同步方法(synchronized)
    
  3.     同步锁(ReenreantLock)
    
  4.     特殊域变量(volatile)
    
  5.     局部变量(ThreadLocal)
    
  6.     阻塞队列(LinkedBlockingQueue)
    
  7.     原子变量(Atomic*)
    

6.4.1. 同步代码块(synchronized)

同步代码块 :

synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。

语法:

synchronized(同步锁){
    //TODO 需要同步操作的代码
}

同步锁:

对象的同步锁只是一个概念,可以想象为在对象上标记了一个锁.

注意:在任何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能在外等着(BLOCKED)。

使用同步代码块代码如下:

package cn.edu.nwafu.safe;

public class Ticket implements Runnable {
    private int ticktNum = 100;

    //定义锁对象
    Object obj = new Object();

    public void run() {
        while(true){
            synchronized (obj){
                if(ticktNum > 0){
                    //1.模拟出票时间
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //2.打印进程号和票号,票数减1
                    String name = Thread.currentThread().getName();
                    System.out.println("线程"+name+"售票:"+ticktNum--);
                }
            }
        }
    }
}

6.4.2. 同步方法(synchronized)

同步方法:

使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。

格式:

public synchronized void method(){
    //TODO 可能会产生线程安全问题的代码 
}

同步锁是谁?

使用同步方法代码如下:

package cn.edu.nwafu.safe;

/**
 * @author shensr
 * @version V1.0
 * @description: 电影票对象
 * @create 2019/9/15
 **/

public class Ticket implements Runnable {

    private int ticketNum = 50;//电影票数量
    private Object obj = new Object(); //锁对象,可以理解为钥匙,拿到钥匙可以执行代码
    //同步方法实现
    public void run() {
        while (true) {
            safeTicket();
        }
    }

    /**
     * 同步方法实现
     * 注意 :对于static方法,同步锁是当前方法所在类的字节码对象(类名.class)。
     * 对于非static方法,同步锁就是this。
     */
    private synchronized void safeTicket() {
        if(ticketNum>0){
                    //邮票,线程睡眠1000ms,售票
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("线程"+Thread.currentThread().getName()+"售票:"+ticketNum--);
                }
    }
}

6.4.3. 同步锁(ReenreantLock)

同步锁:

java.util.concurrent.locks.Lock 机制提供了比synchronized代码块和synchronized方法更广泛的锁定操作,同步代码块/同步方法具有的功能Lock都有,除此之外更强大,更体现面向对象。

同步锁方法:

public void lock() :加同步锁。

public void unlock() :释放同步锁。

使用重入锁代码如下:

package cn.edu.nwafu.safe;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author shensr
 * @version V1.0
 * @description: 电影票对象
 * @create 2019/9/15
 **/

public class Ticket implements Runnable {
    //3. 同步锁实现
    private int ticketNum = 50;//电影票数量
    //定义锁对象(重入锁):构造函数参数为线程是否公平获取锁true-公平;
    //           false-不公平,即由某个线程独占,默认是false
    private Lock lock = new ReentrantLock(true);

    public void run() {
        while (true) {
            lock.lock();//加锁
            try {
                if (ticketNum > 0) {
                    //邮票,线程睡眠1000ms,售票
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("线程" + Thread.currentThread().getName() + "售票:" + ticketNum--);
                }
            }catch (Exception e){

            }finally {
                lock.unlock();//释放锁
            }

        }
    }
}

注意:一定要记得释放锁,否则会引发死锁。

6.5. 小结

Synchronized和Lock区别

7. 线程死锁

7.1. 什么是死锁

多线程以及多进程改善了系统资源的利用率并提高了系统的处理能力。然而,并发执行也带来了新的问题--死锁。

所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。

死锁.png

7.2. 死锁产生的必要条件(一定要熟悉)

以下这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。

7.2.1. 互斥条件

进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。

7.2.2. 不可剥夺条件

进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。

7.2.3. 请求与保持条件

进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。

7.2.4. 循环等待条件

存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, …, pn},其中Pi等 待的资源被P(i+1)占有(i=0, 1, …, n-1),Pn等待的资源被P0占有,如图所示。

循环等待.png
满足条件但无死循环.png

7.2.5. 死锁示例代码

package cn.edu.nwafu.safe;

/**
 * @author shensr
 * @version V1.0
 * @description: 模拟死锁
 * @create 2019/9/15
 **/

public class DeadLockRunnable implements Runnable {
    private static Object obj1 = new Object();//定义成静态变量,使线程可以共享实例
    private static Object obj2 = new Object();//定义成静态变量,使线程可以共享实例
    public int flag ;

    public DeadLockRunnable(int flag) {
        this.flag = flag;
    }

    public void run() {
        if(flag == 1){
            System.out.println(Thread.currentThread().getName()+"已经获取到obj1,正在请求obj2");
            synchronized (obj1){
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (obj2){
                    System.out.println(Thread.currentThread().getName()+"已经获取到obj1和obj2");
                }
            }
        }
        if(flag==2){
            System.out.println(Thread.currentThread().getName()+"已经获取到obj2,正在请求obj1");
            synchronized (obj2){
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (obj1){
                    System.out.println(Thread.currentThread().getName()+"已经获取到obj1和obj2");
                }
            }
        }
    }
}

测试:

package cn.edu.nwafu.safe;


/**
 * @author shensr
 * @version V1.0
 * @description: 测试死锁
 * @create 2019/9/15
 **/

public class DeadLockRunnableTest {

    public static void main(String[] args) {
        //1.创建两个DeadLockRunnable实例
        DeadLockRunnable deadLockRunnable1 = new DeadLockRunnable(1);
        DeadLockRunnable deadLockRunnable2 = new DeadLockRunnable(2);
        //2.创建两个线程执行两个DeadLockRunnable实例
        Thread thread1 = new Thread(deadLockRunnable1, "runnable1");
        Thread thread2 = new Thread(deadLockRunnable2, "runnable2");
        thread1.start();
        thread2.start();
    }

}

执行效果如下:表示死锁产生

runnable1已经获取到obj1,正在请求obj2
runnable2已经获取到obj2,正在请求obj1

7.3. 死锁处理

7.3.1. 死锁预防

预防死锁是设法至少破坏产生死锁的四个必要条件之一,严格的防止死锁的出现。

7.3.1.1. 破坏“互斥”条件

“互斥”条件是无法破坏的。因此,在死锁预防里主要是破坏其他几个必要条件,而不去涉及破坏“互斥”条件。

7.3.1.2. 破坏“占有并等待”条件

破坏“占有并等待”条件,就是在系统中不允许进程在已获得某种资源的情况下,申请其他资源。即要想出一个办法,阻止进程在持有资源的同时申请其他资源。

7.3.1.3. 破坏“不可抢占”条件

破坏“不可抢占”条件就是允许对资源实行抢夺。

7.3.1.4. 破坏“循环等待”条件

破坏“循环等待”条件的一种方法,是将系统中的所有资源统一编号,进程可在任何时刻提出资源申请,但所有申请必须按照资源的编号顺序(升序)提出。这样做就能保证系统不出现死锁。

7.3.2. 死锁避免

避免死锁不严格限制产生死锁的必要条件的存在,因为即使死锁的必要条件存在,也不一定发生死锁。

7.3.2.1. 有序资源分配法

该算法实现步骤如下:

例如:有两个进程P1和P2,有两个资源R1和R2

P1请求资源:R1、R2

P2请求资源:R1、R2

这样就破坏了环路条件,避免了死锁的发生。

7.3.2.2. 银行家算法

银行家算法(Banker's Algorithm)是一个避免死锁(Deadlock)的著名算法,是由艾兹格·迪杰斯特拉在1965年为T.H.E系统设计的一种避免死锁产生的算法。它以银行借贷系统的分配策略为基础,判断并保证系统的安全运行。流程图如下:

银行家算法.png

银行家算法的基本思想是分配资源之前,判断系统是否是安全的;若是,才分配。它是最具有代表性的避免死锁的算法。

设进程i提出请求REQUEST [i],则银行家算法按如下规则进行判断。

  1. 如果REQUEST [i]<= NEED[i,j],则转(2);否则,出错。

  2. 如果REQUEST [i]<= AVAILABLE[i],则转(3);否则,等待。

  3. 系统试探分配资源,修改相关数据:

AVAILABLE[i]-=REQUEST[i];//可用资源数-请求资源数

ALLOCATION[i]+=REQUEST[i];//已分配资源数+请求资源数

NEED[i]-=REQUEST[i];//需要资源数-请求资源数

  1. 系统执行安全性检查,如安全,则分配成立;否则试探险性分配作废,系统恢复原状,进程等待。

7.3.2.3. 顺序加锁

当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。

例如以下两个线程就会死锁:

Thread 1:

lock A (when C locked)

lock B (when C locked)

wait for C

Thread 2:

wait for A

wait for B

lock C (when A locked)

如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。 例如以下两个线程就不会死锁

Thread 1:

lock A

lock B

lock C

Thread 2:

wait for A

wait for B

wait for C

按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需要事先知道所有可能会用到的锁,但总有些时候是无法预知的,所以该种方式只适合特定场景。

7.3.2.4. 限时加锁

限时加锁是线程在尝试获取锁的时候加一个超时时间,若超过这个时间则放弃对该锁请求,并回退并释放所有已经获得的锁,然后等待一段随机的时间再重试

以下是一个例子,展示了两个线程以不同的顺序尝试获取相同的两个锁,在发生超时后回退并重试的场景:

Thread 1 locks A

Thread 2 locks B

Thread 1 attempts to lock B but is blocked

Thread 2 attempts to lock A but is blocked

Thread 1’s lock attempt on B times out

Thread 1 backs up and releases A as well

Thread 1 waits randomly (e.g. 257 millis) before retrying.

Thread 2’s lock attempt on A times out

Thread 2 backs up and releases B as well

Thread 2 waits randomly (e.g. 43 millis) before retrying.

在上面的例子中,线程2比线程1早200毫秒进行重试加锁,因此它可以先成功地获取到两个锁。这时,线程1尝试获取锁A并且处于等待状态。当线程2结束时,线程1也可以顺利的获得这两个锁。

这种方式有两个缺点:

  1.     当线程数量少时,该种方式可避免死锁,但当线程数量过多,这些线程的加锁时限相同的概率就高很多,可能会导致超时后重试的死循环。
    
  2.     Java中不能对synchronized同步块设置超时时间。你需要创建一个自定义锁,或使用Java5中java.util.concurrent包下的工具。
    

7.3.3. 死锁检测

预防和避免死锁系统开销大且不能充分利用资源,更好的方法是不采取任何限制性措施,而是提供检测和解脱死锁的手段,这就是死锁检测和恢复。

死锁检测数据结构:

死锁检测.png

死锁检测步骤:

  1.     寻找一个没有结束标记的进程Pi,对于它而言R矩阵的第i行向量小于或等于A。
    
  2.     如果找到了这样一个进程,执行该进程,然后将C矩阵的第i行向量加到A中,标记该进程,并转到第1步
    
  3.     如果没有这样的进程,那么算法终止
    
  4.     算法结束时,所有没有标记过的进程都是死锁进程。
    

7.3.4. 死锁恢复

利用抢占恢复。

利用回滚恢复

通过杀死进程恢复

8. 线程通讯

8.1. 为什么要线程通信

多个线程并发执行时,在默认情况下CPU是随机切换线程的,有时我们希望CPU按我们的规律执行线程,此时就需要线程之间协调通信。

8.2. 线程通讯方式

线程间通信常用方式如下:

Object的wait、notify、notifyAll

Condition的await、signal、signalAll

8.2.1. 休眠唤醒方式

多线程打印10以内的奇偶数:

i从0开始,当i是奇数时,奇数线程打印,偶数线程等待;

​ 当i是偶数时,偶数线程打印,奇数线程等待。

方式一:Object的wait、notify、notifyAll

package cn.edu.nwafu.communication;

/**
 * @author shensr
 * @version V1.0
 * @description: **多线程打印10以内的奇偶数:**
 *                  i从0开始,当i是奇数时,奇数线程打印,偶数线程等待;
 * ​                         当i是偶数时,偶数线程打印,奇数线程等待。 
 *              使用Object的wait、notify、notifyAll方式实现线程通讯
 * @create 2019/9/16
 **/

public class WaitNotifyRunnable {
    private Object obj = new Object();
    private Integer i=0;
    
    public void odd() {
        while(i<10){
            synchronized (obj){
                if(i%2 == 1){
                    System.out.println("奇数:"+i);
                    i++;
                    obj.notify();
                } else {
                    try {
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

    public void even(){
        while(i<10){
            synchronized (obj){
                if(i%2 == 0){
                    System.out.println("偶数:"+i);
                    i++;
                    obj.notify();
                } else {
                    try {
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
    public static void main(String[] args){
        final WaitNotifyRunnable runnable = new WaitNotifyRunnable();
        Thread t1 = new Thread(new Runnable() {
            public void run() {
                runnable.odd();
            }
        }, "偶数线程");
        //lambda表达式
        Thread t2 = new Thread(() -> runnable.even(), "奇数线程");

        t1.start();
        t2.start();
    }
}

注意: Object的wait、notify、notifyAll这些方法依赖于synchronized关键字,没有就会抛出java.lang.IllegalMonitorStateException异常

方式二:Condition的await、signal、signalAll

package cn.edu.nwafu.communication.conditon;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author shensr
 * @version V1.0
 * @description:
 * @create 2019/9/16
 **/

public class WaitNotifyRunnable {

    private Lock lock = new ReentrantLock();//这里要设置为独占锁,参数要为false
    private Condition condition = lock.newCondition();
    private Integer i=0;
    public void odd() {
        while(i<10){
            lock.lock();
            try{
                if(i%2 == 1){
                    System.out.println("奇数:"+i);
                    i++;
                    condition.signal();
                } else {
                    condition.await();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }

        }
    }

    public void even(){
        while(i<10){
            lock.lock();
            try{
                if(i%2 == 0){
                    System.out.println("偶数:"+i);
                    i++;
                    condition.signal();
                } else {
                    condition.await();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }

        }
    }
    public static void main(String[] args){
        final WaitNotifyRunnable runnable = new WaitNotifyRunnable();
        Thread t1 = new Thread(()->runnable.odd(), "偶数线程");
        Thread t2 = new Thread(() -> runnable.even(), "奇数线程");
        t1.start();
        t2.start();
    }
}

Object和Condition休眠唤醒区别

8.2.2. CountDownLatch方式

CountDownLatch是在java1.5被引入的,存在于java.util.concurrent包下。

CountDownLatch这个类能够使一个线程等待其他线程完成各自的工作后再执行。

CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量

CountDownLatch方式.png

每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。

示例代码:

package cn.edu.nwafu.communication.countDownLatch;

import java.util.concurrent.CountDownLatch;

/**
 * @author shensr
 * @version V1.0
 * @description: CountDownLatch方式
 * @create 2019/9/16
 **/

public class CountDown {
    private Integer i = 0;
    private CountDownLatch countDownLatch = new CountDownLatch(1);

    public void odd(){
        while(i < 10){
            if(i%2 == 1){
                System.out.println("奇数:"+i);
                i++;
                countDownLatch.countDown();
            } else {
                try {
                    countDownLatch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public void even(){
        while(i < 10){
            if(i%2 == 0){
                System.out.println("偶数:"+i);
                i++;
                countDownLatch.countDown();
            } else {
                try {
                    countDownLatch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public static void main(String[] args){
        final CountDown countDown = new CountDown();
        Thread t1 = new Thread(() -> countDown.odd(),"奇数");
        Thread t2 = new Thread(() -> countDown.even(),"偶数");

        t1.start();
        t2.start();
    }
}

8.2.3. CyclicBarrier方式

CyclicBarrier是在java1.5被引入的,存在于java.util.concurrent包下。

CyclicBarrier实现让一组线程等待至某个状态之后再全部同时执行。

CyclicBarrier底层是基于ReentrantLock和Condition实现。

三个线程同时启动,示例代码如下:

package cn.edu.nwafu.communication.cyclicbarrier;

import java.util.Date;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

/**
 * @author shensr
 * @version V1.0
 * @description:
 * @create 2019/9/16
 **/

public class CyclicBarrierDemo {
     private static CyclicBarrier cyclicBarrier = new CyclicBarrier(3);

    public static void run(){
        System.out.println(Thread.currentThread().getName()+":准备...");
        try {
            cyclicBarrier.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"启动完毕:"+new Date().getTime());

    }

    public static void main(String[] args){
        new Thread(() -> run(),"线程1").start();
        new Thread(() -> run(),"线程2").start();
        new Thread(() -> run(),"线程3").start();
    }
}

执行效果如下:三个线程同时启动

线程1:准备...
线程2:准备...
线程3:准备...
线程3启动完毕:1568605543873
线程1启动完毕:1568605543873
线程2启动完毕:1568605543873

8.2.4. Semaphore方式

Semaphore是在java1.5被引入的,存在于java.util.concurrent包下。

Semaphore用于控制对某组资源的访问权限。

工人使用机器工作,示例代码如下:

package com.multithread.thread;

import java.util.concurrent.Semaphore;

public class SemaphoreDemo {

    static class Machine implements Runnable{
        private int num;
        private Semaphore semaphore;

        public Machine(int num, Semaphore semaphore) {
            this.num = num;
            this.semaphore = semaphore;
        }

        public void run() {
            try {
                semaphore.acquire();//请求机器
                System.out.println("工人"+this.num+"请求机器,正在使用机器");
                Thread.sleep(1000);
                System.out.println("工人"+this.num+"使用完毕,已经释放机器");
                semaphore.release();//释放机器
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args){
        int worker = 8;//工人数
        Semaphore semaphore = new Semaphore(3);//机器数
        for (int i=0; i< worker; i++){
            new Thread(new Machine(i, semaphore)).start();
        }
    }
}

执行效果如下:

工人[1]请求机器,正在使用机器...
工人[0]请求机器,正在使用机器...
工人[2]请求机器,正在使用机器...
工人[0]使用完毕,已经释放机器!!!
工人[1]使用完毕,已经释放机器!!!
工人[3]请求机器,正在使用机器...
工人[4]请求机器,正在使用机器...
工人[2]使用完毕,已经释放机器!!!
工人[6]请求机器,正在使用机器...
工人[3]使用完毕,已经释放机器!!!
工人[4]使用完毕,已经释放机器!!!
工人[5]请求机器,正在使用机器...
工人[7]请求机器,正在使用机器...
工人[6]使用完毕,已经释放机器!!!
工人[7]使用完毕,已经释放机器!!!
工人[5]使用完毕,已经释放机器!!!

8.3 小结

8.3.1. sleep和wait区别

wait sleep
同步 只能在同步上下文中调用wait方法,否则抛出java.lang.IllegalMonitorStateException异常 不需要在同步方法或同步代码块中调用
作用对象 wait方法定义在Object类中,作用于对象本身 sleep方法定义在java.lang.Thread中,作用于当前线程
释放锁资源
唤醒条件 其他线程调用对象的notify()方法或则notifyAll()方法 超时或则调用interrupt方法
方法属性 wait是实例方法 sleep是静态方法

8.3.2. wait和notify区别

上一篇下一篇

猜你喜欢

热点阅读