Java高级编程--多线程(二)

多线程程序将单个任务按照功能分解成多个子任务来执行,每个子任务称为一个线程,多个线程共同完成主任务的运行过程,这样可以缩短用户等待时间,提高服务效率。本篇博客将继续介绍Java开发中多线程的使用。

↷Java高级编程--多线程(一)


目录:

☍ 线程的生命周期

☍ 线程的同步

☍ 线程的通信

☍ JDK5.0新增线程创建方式


☍ 线程的生命周期

▾ Thread.State类定义了线程的几种状态

要想实现多线程,必须在主线程中创建新的线程对象。Java语言使用Thread类
及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态

新建: 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态

就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源

运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run()方法定义了线程的操作和功能

阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中
止自己的执行,进入阻塞状态

死亡:线程执行完了所有工作或线程被提前强制性地中止或出现异常导致结束


☍ 线程的同步

多个线程执行的不确定性引起执行结果的不稳定。多个线程对资源的共享,会造成操作的不完整性,会破坏数据。此时需要线程的同步来解决问题。

如窗口售票:

售票代码:

class SellTicket extends Thread{
    private static int ticketNum = 100;
    public SellTicket(String windownName){
        super.setName(windownName);
    }
    @Override
    public void run() {
        while(true){
            if(ticketNum > 0){
                try {
                    sleep(100);  //阻塞,放大线程安全问题
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(getName() + "窗口卖票,票号为:" + ticketNum--);
            }else{
                System.out.println("票已卖光");
                break;
            }
        }
    }
}
public class Thread_SellTicket {
    public static void main(String[] args) {
        SellTicket t1 = new SellTicket("窗口1");
        SellTicket t2 = new SellTicket("窗口2");
        SellTicket t3 = new SellTicket("窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

结果(存在线程安全问题):

▾ 线程安全问题的探析(同步机制)

提出问题:线程安全问题,如卖票的多线程中,出现了重票和错票

问题原因:当某个线程操作尚未完成时,其他线程进入操作了共享的资源,导致共享数据的错误

解决思路:当一个线程a在操作共享资源时,其他线程不能参与进来, 直到线程a操作完成时,其他线程才可以开始操作共享资源。这种情况即使线程a出现了阻塞也不能改变。

解决方法:在Java中,通过同步机制来解决线程的安全问题(synchronized)

☃ 方法一:同步代码块

☃ 方法二:同步方法

同步机制一:同步代码块

synchronized (同步监视器/锁:对象){
	// 需要被同步的代码;
}

//如在窗口售票例子中通过Runnable接口实现的多线程中使用同步代码块
public void run() {
        while(true){
            synchronized(this){//synchronized(obj){
                //this指当前线程对象,在runnable实现方式中唯一(runnable对象传入Thread构造器中需唯一)
                if(ticketNum > 0){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "窗口卖票,票号为:" + ticketNum--);
                }else{
                    System.out.println("票已卖光");
                    break;
                }
            }
        }
    }

//如在窗口售票例子中通过继承Thread类实现的多线程中使用同步代码块
public void run() {
        while(true){
            synchronized(TS_SellTicket.class){ //synchronized(obj){  类的class也是对象,TS_SellTicket为继承Thread的子类
                if(ticketNum > 0){
                    try {
                        sleep(100);//阻塞
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(getName() + "窗口卖票,票号为:" + ticketNum--);
                }else{
                    System.out.println("票已卖光");
                    break;
                }
            }
        }
    }

同步机制二:同步方法

如果操作的共享数据的代码完整的声明在一个方法中,可以将这个方法声明为同步的

权限修饰符 synchronized 方法名(){//操作共享数据的代码}
    
//如在窗口售票例子中通过Runnable接口实现的多线程中使用同步方法
    public void run() {
        while(true){
            printInfo();
            if (ticketNum<=0){
                System.out.println("票已售完");
                break;
            }
        }
    }
    public synchronized void printInfo(){
        if(ticketNum > 0){
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "窗口卖票,票号为:" + ticketNum--);
        }
    }
    
//如在窗口售票例子中通过继承Thread类实现的多线程中使用同步代方法
    public void run() {
        while(true){
            printInfo();
            if(ticketNum <= 0){
                System.out.println("票已售光");
            }
        }
    }
//    public synchronized void printInfo(){   同步监视器不唯一
    public static synchronized void printInfo(){  //同步监视器默认为当前类(Thread子类.class)
        try {
            sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if(ticketNum > 0){
            System.out.println(Thread.currentThread().getName() + "窗口卖票,票号为:" + ticketNum--);
        }
    }

synchronized的监视器/锁

☄ 任意对象都可以作为同步锁

☄ 同步方法的锁(默认):静态方法(类名.class)、非静态方法(this)

☄ 同步代码块:自己指定(如Object obj),很多时候也是指定为this(runnable接口实现类方式)或类名.class(Thread继承方式)

说明

☃ 操作共享数据的代码为需要同步的代码,不能多也不能少

☃ 共享数据:多个线程共同操作的变量。(售票例子中为票ticket)

☃ 同步监视器/同步锁:任何一个类的对象都可以充当锁。

➥ 多个线程必须共用一把锁(同一个对象)

➥ 一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方
法共用同一把锁(this),同步代码块(指定需谨慎)

确定同步机制的使用

◌ 1、代码是否存在线程安全?

  • 明确哪些代码是多线程运行的代码
  • 明确多个线程是否有共享数据
  • 明确多线程运行代码中是否有语句操作共享数据

◌ 2、代码存在线程安全如何解决?

  • 对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。即所有操作共享数据的这些语句都要放在同步范围中

◌ 2、注意选中要同步的代码范围

  • 范围太小:没锁住所有有安全问题的代码
  • 范围太大:没发挥多线程的功能。

同步锁的释放

☃ 当前线程的同步方法/同步代码块执行结束或执行了线程对象的stop()方法。

☃ 当前线程在同步代码块、同步方法中遇到break/return终止了该代码块/该方法的继续执行。

☃ 当前线程在同步代码块/同步方法中出现了未处理的Error或Exception,导致异常结束。

☃ 当前线程在同步代码块/同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁。

不会释放锁的说明

➥ 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行,此时只是让出了cpu资源,当前线程并未结束

➥ 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器),需注意的是应尽量避免使用suspend()和resume()来控制线程

单例模式之懒汉式(线程安全补充)

单列模式详解见链接↷Java面向对象--单例(Singleton)设计模式和main方法

class Bank extends Thread{
    private Bank(){}   //私有化构造器
    private static Bank instance = null;   //私有静态对象变量,先步new出实例对象
//    public static synchronized Bank getInstance(){
    public static Bank getInstance(){
        //提供获取私用静态对象的方法,若静态单例对象为null,new出实例对象,否则直接返回
        if (instance == null) {  //效率更高
            synchronized (Bank.class) {
                if(instance == null){
                    instance = new Bank();
                }
            }
        }
        return instance;
    }

    @Override
    public void run() {
        getInstance();
    }
}
public class Singleton_ThreadSave {
    public static void main(String[] args) {

    }
}

线程的死锁问题

◌ 死锁原因

  不同的线程分别占用对方需要的同步资源,并且都在等待对方放弃自己需要的同步资源,此时就形成了线程的死锁

◌ 死锁状态

  出现死锁后,不会出现异常,不会有错误提示,只是所有线程都处于阻塞状态,无法继续运行,程序无法结束

◌ 解决方法

专门的算法逻辑;尽量减少同步资源的定义;尽量避免多层嵌套同步

死锁代码演示:

public class Dead_Lock {
    public static void main(String[] args) {
        dead_lock();
    }
    public static void dead_lock(){
        StringBuffer s1 = new StringBuffer();
        StringBuffer s2 = new StringBuffer();

        new Thread(){
            @Override
            public void run() {
                synchronized (s1){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    s1.append("c");
                    s2.append("3");
                    synchronized (s2){
                        s1.append("d");
                        s2.append("4");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }.start();

       //使用匿名对象创建线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (s2){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    s1.append("a");
                    s2.append("1");
                    synchronized (s1){
                        s1.append("b");
                        s2.append("2");
                        System.out.println(s1);
                        System.out.println(s2);
                    }
                }
            }
        }).start();
    }
}

同步机制三:Lock锁

☃ 从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。

☃ java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象

☃ ReentrantLock 类实现了 Lock ,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。

Lock锁使用代码演示:

class L_Thread implements Runnable{
    private int ticket = 100;
    //1、定义lock锁
    private ReentrantLock lock = new ReentrantLock(true);
    //参数为boolean型,线程是否先到先执行,不会出现一个线程连续执行多次
    @Override
    public void run() {
        while (true){
            try{
                //2、调用lock锁
                lock.lock();
                if (ticket > 0){
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "窗口卖票,票号为:" + ticket--);
                }else {
                    break;
                }
            }finally {
                //3、调用unlock释放锁
                lock.unlock(); 
            }
        }
    }
}
public class LockTest {
    public static void main(String[] args) {
        L_Thread l = new L_Thread();
        Thread t1 = new Thread(l,"窗口1");
        Thread t2= new Thread(l,"窗口2");
        Thread t3 = new Thread(l,"窗口3");
        t1.start();
        t2.start();
        t3.start();
    }
}

注意: 加锁后不要忘记释放锁,如果同步代码有异常,要将unlock()写入finally语句块

synchronized与Lock的对比

◌ 相同点

 二者都能解决线程的安全问题

◌ 不同点

  • Lock是显式锁(lock需要手动的启动同步锁(lock()),同时结束同步也需要手动调用unlock()释放锁),synchronized是隐式锁(执行时自动启动同步监视器/锁,在执行完相应的同步代码以后/出了作用域自动释放同步监视器/锁)
  • Lock只有代码块锁,synchronized有代码块锁和方法锁
  • 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有
    更好的扩展性(提供更多的子类)

◌ 三种同步机制的使用优先级(视具体情况而定)

 Lock锁 ➠ 同步代码块(已经进入了方法体,分配了相应) ➠ 同步方法
(在方法体之外)


☍ 线程的通信

 线程的通信通过wait()方法和notify()/notifyAll()方法实现

通过线程通信使用线程1和线程2交替打印1-100:

交替打印也可通过ReentrantLock类的ReentrantLock(true)构造器实现

class TC_Thread implements Runnable{
    private int index = 1;
    @Override
    public void run() {
        while(true){
            synchronized (this){
                notify();    //唤醒被wait的线程
                if (index <= 100) {
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":" + index++);
                    try {
                        wait();   //阻塞当前线程
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else{
                    break;
                }
            }
        }
    }
}
public class ThreadCommunication {
    public static void main(String[] args) {
        TC_Thread tc = new TC_Thread();
        Thread t1 = new Thread(tc,"线程1");
        Thread t2 = new Thread(tc,"线程2");
        t1.start();
        t2.start();
    }
}

wait()与notify()和notifyAll()

因为这三个方法必须由锁对象调用,而任意对象都可以作为synchronized的同步锁,
因此这三个方法声明在Object类中

wait():一旦执行此方法,令当前线程挂起并放弃CPU、同步资源进行等待,使别的线程可访问并修改共享资源,当前线程就进入阻塞状态,并释放同步监视器,等候其他线程调用notify()或notifyAll()方法唤醒,唤醒后等待重新获得对监视器的所有权后才能继续执行。

notify():一旦执行此方法,就会唤醒被wait的一个线程,如果有多个线程被wait,就唤醒优先级高的线程

notifyAll():一旦执行此方法,就会唤醒被wait的所有线程

wait()方法的使用

☃ 在当前线程中调用方法:对象名.wait()。

☃ 使当前线程进入等待(某对象)状态,直到另一线程对该对象发出 notify(或notifyAll)为止。。

☃ 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)。

☃ 调用此方法后,当前线程将释放对象监控权,然后进入等待。

☃ 在当前线程被notify后,要重新获得监控权,然后从断点处继续代码的执行。

notify()/notifyAll()方法的使用

☃ 在当前线程中调用方法: 对象名.notify()。

☃ 功能:唤醒等待该对象监控权的一个/所有线程。

☃ 调用方法的必要条件:当前线程必须具有对该对象的监控权(加锁)。

注意:这三个方法只有在synchronized方法或synchronized代码块中才能使用,否则会报
java.lang.IllegalMonitorStateException异常

注意:wait()、notify()、notifyAll()三个方法的调用者必须与同步代码块或同步方法中的同步监视器/锁相同,否则会出现 IllegalMonitorStateException异常

sleep()和wait()方法的异同点

◌ 相同点

 一旦执行方法,都可以使得当前线程进入阻塞状态

◌ 不同点

两个方法的声明位置不同:Thread类中声明sleep(),Object类中声明wait();

调用场景不同:sleep()可以在任何需要的场合调用,wait()只能在synchronized同步代码块或同步方法中调用

是否释放同步监视器:如果两个方法都使用在synchronized中,sleep()不会自动释放锁,wait()会自动释放锁


☍ JDK5.0新增线程创建方式

▾ 实现Callable接口方式创建线程

Runnable接口和Callable接口对比

◌ 相比run方法,call()方法有返回值,在需要获取返回值的情况下很适用

◌ 可以抛出异常从而被外层的操作获取异常信息

◌ callable是支持泛型的返回值

◌ Callable需要借助FutureTask类,比如获取返回结果

Future接口

◌ 可以对具体Runnable、Callable任务的执行结果进行取消、查询是
否完成、获取结果等。

◌ FutrueTask是Futrue接口的唯一的实现类

◌ FutureTask 同时实现了Runnable、Future接口。它既可以作为
Runnable被线程执行,又可以作为Future得到Callable的返回值

Callable接口方式实现线程步骤

↬ 创建Callable实现类,定义Callable实现类对象c

↬ 定义FutureTask对象f,将Callable实现类对象c作为FutureTask构造器的参数

↬ 定义Thread线程对象t,将FutureTask对象f作为Thread构造器的参数

↬ Thread对象t调用start()方法启动线程

↬ 若要获得Callable实现类对象的call()方法返回值,需启动线程后使用FutureTask对象f调用get()方法

Callable接口实现多线程代码演示:

//1、创建Callable实现类
class CT_Thread implements Callable {
    //2、实现call()方法
    @Override
    public Object call() throws Exception {
        int sum = 0;
        for (int i = 0; i <= 100; i++) {
            if(i % 2 == 0){
                System.out.println(i);
                sum += i;
            }
        }
        return sum;
    }
}
public class CallableTest {
    public static void main(String[] args) {
        //3、定义Callable接口实现类对象
        CT_Thread ct = new CT_Thread();
        //4、定义FutureTask对象,并将Callable接口实现类对象作为FutureTask构造器的参数
        FutureTask fu= new FutureTask(ct);   //FutureTask构造器参数为Callable实现类对象
        //5、定义Thread对象,将FutureTask对象作为参数传递到Thread类的构造器中,Thread对象调用start()方法启动线程
        Thread t1 = new Thread(fu);
        t1.start();
        try {
            //6、FutureTask对象的get()方法返回值为构造器参数Callable实现类重写的call()的返回值(可省略此步,需要获取call()方法返回值时写)
            Object sum = fu.get();
            System.out.println("总和为:"+sum);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

▾ 线程池方式创建线程

经常创建和销毁、使用量特别大的资源,比如并发情况下的线程,对性能影响很大。提前创建好多个线程,放入线程池中,使用时直接获取,使用完
放回池中。可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具。

使用线程池的优点

◌ 提高响应速度(减少了创建新线程的时间)

◌ 降低资源消耗(重复利用线程池中线程,不需要每次都创建)

◌ 便于线程管理(提供了很多方法)

  ↬ corePoolSize:核心池的大小

  ↬ maximumPoolSize:最大线程数

  ↬ keepAliveTime:线程没有任务时最多保持多长时间后会终止

  ↬ ......

线程池相关API

☃ JDK 5.0起提供了线程池相关API:ExecutorService 和 Executors。

☃ ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor。

   ↬ void execute(Runnable command) :执行任务/命令,没有返回值,一般用来执行
Runnable

   ↬  Future submit(Callable task):执行任务,有返回值,一般又来执行
Callable

   ↬ void shutdown() :关闭连接池

☃ Executors:工具类、线程池的工厂类,用于创建并返回不同类型的线程池。

  ↬ Executors.newCachedThreadPool():创建一个可根据需要创建新线程的线程池

↬ Executors.newFixedThreadPool(n); 创建一个可重用固定线程数的线程池

↬ Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池

↬ Executors.newScheduledThreadPool(n):创建一个线程池,它可安排在给定延迟后运
行命令或者定期地执行。

线程池方式创建线程代码演示:

class NumberThread implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            if(i % 2 == 0){
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    }
}

class NumberThread1 implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i <= 100; i++) {
            if(i % 2 != 0){
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    }
}

public class ThreadPool {
    public static void main(String[] args) {
        //1、提供指定线程数量的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);

        //设置线程池的属性
        ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
        service1.setCorePoolSize(15);  //核心池的大小
        service1.setMaximumPoolSize(10); //最大线程数
        //2、执行指定线程的操作,,需要提供实现Runnable接口或Callable接口实现类的对象作为execute的参数
        service.execute(new NumberThread());   //适合于Runnable
        service.execute(new NumberThread1());

        //service.submit();     //适合于Callable

        service.shutdown();   //关闭线程池
    }
}

本博客与CSDN博客༺ཌ༈君☠纤༈ད༻同步发布

posted @ 2020-06-06 23:22  ༺ཌ༈君☠纤༈ད༻  阅读(273)  评论(0编辑  收藏  举报