当前位置:首页>笔记分享>Java笔记>Java多线程(一)

Java多线程(一)

一、线程概述

1. 什么是进程

进程是系统进行资源分配的基本单位也是独立运行的基本单位。多个进程可以同时存在于内存中,能在一段时间内同时运行,在windows操作中,可以打开任务管理器看到各种各样的进程和对应的PID,并且都占用了一定的系统资源。单核CPU在同一个时刻,只能运行一个进程。所谓同时运行是宏观上的概念,微观上进程之间是在不停地快速切换。

补充:

进程具有几个基本特性:

  • 动态性。进程是程序在处理器上的一次执行过程,它因创建而产生,由调度而执行,因得不到资源而暂停,最后因撤销而消亡。
  • 并发性:多个进程可以同时存在于内存中,能在一段时间内同时运行。进程的目的是使程序能与其他程序并行,以提高资源利用率。
  • 独立性:进程是一个能独立运行的基本单位,也是系统进行资源分配和调度的独立单位。
  • 异步性:进程以各自独立的、不可预知的速度向前推进。
  • 结构特征:为了描述和记录进程的运动变化过程,并使之能正确运行,每个进程都由程序段、数据段和一个进程控制块(Process Control Block,PCB)组成。

系统根据PCB感知进程的存在。PCB是进程存在的唯一标志。

2. 什么是线程

线程又称轻量级进程(Light Weight Process),它是进程内一个相对独立的、可调度的执行单元,也是CPU的基本调度单位。一个进程由一个或多个线程组成,彼此间完成不同的工作,同时执行,称为多线程,此处的同时执行也是宏观上的。在windows操作系统中,可以打开任务管理器,找到性能分页下的资源管理器,可以查看每个进程所拥有的线程数。

JAVA虚拟机是一个进程,当中默认包含主线程(main),可通过代码创建多个独立线程,与main并发执行。

补充:

  • 线程的引入

    在操作系统中引入线程,是为了减少程序并发执行时所付出的时空开销,使操作系统具有更好的并发性,为了说明这一点,先来回顾一下进程的两个基本属性:

    1. 进程是一个拥有资源的独立单位。
    2. 进程同时又是一个可以被处理器独立调度和分配的单元。

    上述两个属性构成了程序并发执行的基础。然而,为了使进程能并发执行,操作系统还必须进行一系列的操作,如进程的创建、撤销进程和进程切换。在进行这些操作时,操作系统要为进程分配资源及回收资源,为运行进程保存现场信息,这些工作都需要付出较多的时空开销。为了使多个程序更好地并发执行,并尽量减少操作系统的开销,操作系统设计者考虑将进程的两个属性分离开来,让线程去完成第二个基本属性的任务,而进程只完成第一个基本属性的任务。

  • 线程的定义

    线程的定义存在多种不同的提法,前文概述中已阐述一二,此处进行补充说明。线程本身不能单独运行,只能包含在进程中,只能在进程中执行。线程自己基本上不拥有资源,只拥有一点在运行时必不可少的资源,但它可以与同属一个进程的其他线程共享该进程资源。多线程是指一个进程中有多个线程,这些线程共享该进程资源。如果一个线程修改了一个数据项,其他线程可以了解和使用此结果数据。一个线程打开并读一个文件时,同一进程中的其他线程也可以同时读此文件。

3. 进程和线程的区别

  1. 进程是操作系统资源分配的基本单位,而线程是CPU的基本调度单位。
  2. 一个程序运行后之后有一个进程。
  3. 一个进程可以包含多个线程,但是至少需要有一个线程,否则这个线程是没有意义的。
  4. 进程间不能共享数据段地址,但同进程的线程之间可以。

4. 线程的组成

  • 任何一个线程都具有基本的组成部分:
    • CPU时间片:操作系统会为每个线程分配执行时间。
    • 运行数据:
    • 堆空间:存储线程需要使用的对象,多个线程可以共享堆中的对象。
    • 栈空间:存储线程需要使用的局部变量,每个线程都拥有独立的栈。
    • 线程的逻辑代码

5. 线程的特点

  • 线程抢占式执行。
    • 效率高。
    • 可防止单一线程长时间独占CPU。
  • 在单核CPU中,宏观上同时执行,微观上顺序执行。

二、线程的创建

  • 创建线程的三种方式:
    1. 继承Thread类,重写run方法,调用start开启线程。
    2. 实现Runnable接口。 也要重写run方法,之后创建线程对象调用start方法启动线程
    3. 实现Callable接口。

1. 创建线程(一)

package Demo01;

public class TestThread01 extends Thread {
    @Override
    public void run() {
        //run线程方法体
        for (int i = 0; i < 50; i++) {
            System.out.println("我是run=="+i);
        }
    }
//main线程
    public static void main(String[] args) {
        //创建一个线程对象
        TestThread01 testThread01 = new TestThread01();
        //调用start()方法开启线程
        testThread01.start();
        for (int i = 0; i < 500; i++) {
            System.out.println("我是main=="+i);
        }
    }
}

每次运行后得到的结果都不一样,而且主线程和子线程都是交替执行的,并且是抢占式执行。

需要注意的是,在main方法中需要调用线程类的start方法来启动线程,如果调用run方法就相当于调用了一个普通类中的方法,那么还是由主线程执行。

2. 获取和修改线程名称

  • 获取线程ID和线程名称

    1. 在Thread的子类中调用this.getId()this.getName()
    2. 使用Thread.currentThread().getId()Thread.currentTread().getName()
/**
 * 线程类
 * 获取线程名方法演示
 */
public class MyThread extends Thread{
    @Override
    public void run() {
        for(int i=0;i<10;i++) {
            //第一种方法
            System.out.println("线程ID:"+this.getId()+" "+"线程名:"+this.getName()+" "+i);
            //第二种方法
            //System.out.println("线程ID:"+Thread.currentThread().getId()+" "+"线程名:"+Thread.currentThread().getName());
        }
    }
}

使用第一种方法的线程类必须继承Thread父类,否则不能使用这两个方法。

第二种方法调用的静态方法currentThread表示获取当前线程,哪个线程执行的当前代码就获取谁。

testMyThread类中再新创建一个线程类对象并启动,可以看到如下结果:

主线程:0
线程ID:11 线程名:Thread-1 0
线程ID:10 线程名:Thread-0 0
线程ID:11 线程名:Thread-1 1
主线程:1
线程ID:11 线程名:Thread-1 2
线程ID:10 线程名:Thread-0 1
线程ID:10 线程名:Thread-0 2
线程ID:10 线程名:Thread-0 3
线程ID:10 线程名:Thread-0 4
线程ID:10 线程名:Thread-0 5
线程ID:10 线程名:Thread-0 6
线程ID:10 线程名:Thread-0 7
线程ID:10 线程名:Thread-0 8
线程ID:10 线程名:Thread-0 9
线程ID:11 线程名:Thread-1 3
线程ID:11 线程名:Thread-1 4
线程ID:11 线程名:Thread-1 5
线程ID:11 线程名:Thread-1 6
线程ID:11 线程名:Thread-1 7
线程ID:11 线程名:Thread-1 8
线程ID:11 线程名:Thread-1 9
主线程:2
主线程:3
主线程:4
主线程:5
主线程:6
主线程:7
主线程:8
主线程:9
  • 修改线程名称

    1. 调用线程对象的setName()方法。
    2. 使用线程子类的构造方法赋值。
    //使用setName方法
    myThread.setName("子线程1");
    myThread.start();
    myThread2.setName("子线程2");
    myThread2.start();

    使用该方法需要注意必须在线程启动之前修改线程名,否则就没有意义了。

//使用构造方法
public class MyThread extends Thread{
    public MyThread() {     
    }
    public MyThread(String name) {
        super(name);
    }
    @Override
    public void run() {
        //略
    }
}

线程类的带参构造方法调用了父类的构造方法,也可以把name赋给线程名。

//创建线程对象
MyThread myThread=new MyThread("子线程1");
MyThread myThread2=new MyThread("子线程2");

在创建线程对象时可以直接通过构造方法为线程修改名字,运行后结果如下:

主线程:0
线程ID:11 线程名:子线程2 0
线程ID:11 线程名:子线程2 1
线程ID:11 线程名:子线程2 2
线程ID:11 线程名:子线程2 3
线程ID:11 线程名:子线程2 4
线程ID:11 线程名:子线程2 5
线程ID:10 线程名:子线程1 0
线程ID:11 线程名:子线程2 6
主线程:1
线程ID:11 线程名:子线程2 7
线程ID:10 线程名:子线程1 1
线程ID:11 线程名:子线程2 8
主线程:2
线程ID:11 线程名:子线程2 9
线程ID:10 线程名:子线程1 2
线程ID:10 线程名:子线程1 3
线程ID:10 线程名:子线程1 4
线程ID:10 线程名:子线程1 5
主线程:3
主线程:4
主线程:5
线程ID:10 线程名:子线程1 6
主线程:6
主线程:7
主线程:8
线程ID:10 线程名:子线程1 7
主线程:9
线程ID:10 线程名:子线程1 8
线程ID:10 线程名:子线程1 9

3. 一个线程小案例

案例1:

package com.gong.Demo01;

/**
 * 四个窗口各自卖票
 */
public class TicketWinTest05 extends Thread {
    private int ticket = 100;
    public TicketWinTest05(String name) {
        super(name);
    }

    @Override
    public void run() {
        while(true){
            if (ticket>0){
                ticket--;
                System.out.println(Thread.currentThread().getName()+"卖出了一张票,还有"+ticket+"张");
            }else {
                break;
            }
        }
    }
    public static void main(String[] args) {
        TicketWinTest05 t1 = new TicketWinTest05("窗口1");
        TicketWinTest05 t2 = new TicketWinTest05("窗口2");
        TicketWinTest05 t3 = new TicketWinTest05("窗口3");
        TicketWinTest05 t4 = new TicketWinTest05("窗口4");
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

运行代码后结果如下:

窗口1卖出了一张票,还剩99张。
窗口3卖出了一张票,还剩99张。
窗口1卖出了一张票,还剩98张。
窗口2卖出了一张票,还剩99张。
窗口1卖出了一张票,还剩97张。
窗口1卖出了一张票,还剩96张。
窗口1卖出了一张票,还剩95张。
窗口3卖出了一张票,还剩98张。
窗口1卖出了一张票,还剩94张。
窗口4卖出了一张票,还剩99张。
窗口2卖出了一张票,还剩98张。
窗口4卖出了一张票,还剩98张。
窗口1卖出了一张票,还剩93张。
窗口3卖出了一张票,还剩97张。
窗口1卖出了一张票,还剩92张。
窗口4卖出了一张票,还剩97张。
窗口2卖出了一张票,还剩97张。
窗口4卖出了一张票,还剩96张。
窗口1卖出了一张票,还剩91张。
窗口3卖出了一张票,还剩96张。
//略

案例2

package com.gong.Demo01;
//利用多线程进行网络图片下载
//需要下载commons-io包导入使用
import com.sun.org.apache.xalan.internal.xsltc.dom.CurrentNodeListFilter;
import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;

public class TestThread02 extends Thread {
    private String url;//保存网络图片地址
    private String name;//保存的图片名

    public TestThread02(String url, String name) {
        this.url = url;
        this.name = name;
    }

    @Override
    public void run() {
        WebDownLoader webDownLoader = new WebDownLoader();
        webDownLoader.downLoader(url, name);
        System.out.println("下载了文件名为:"+name);
    }

    public static void main(String[] args) {
        TestThread02 t1 = new TestThread02("http://image.qianye88.com/pic/100dc4c31a4cfa4c6d3def642fb2efaf?imageMogr2/thumbnail/x280/quality/90!","1.jpg");
        TestThread02 t2 = new TestThread02("http://image.qianye88.com/pic/fb803f9e848476306bac335c1073e0a0?imageMogr2/thumbnail/x280/quality/90!","3.jpg");
        TestThread02 t3 = new TestThread02("http://image.qianye88.com/pic/28f7f2ed45594a7952b9511a0fcd3fc1?imageMogr2/thumbnail/x280/quality/90!","2.jpg");
        t1.start();
        t2.start();
        t3.start();
    }
}
//下载器
class WebDownLoader {
    public void downLoader(String url,String name){
        try {
          //commons-io包中的方法实现对图片的拷贝
            FileUtils.copyURLToFile(new URL(url), new File(name));
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("downLoader异常");
        }
    }
}

4. 创建线程(二)

package com.gong.Demo01;
//创建线程方式2:实现Runnable接口,重写run方法,执行线程需要丢入Runnable实现类,调用start
public class TestThread03 implements Runnable {
    @Override
    public void run() {
        //run线程方法体
        for (int i = 0; i < 200; i++) {
            System.out.println("主线程:"+i);
        }
    }
    //main线程
    public static void main(String[] args) {
        //创建runnable接口的实现类对象
        TestThread03 testThread03 = new TestThread03();
        //创建线程对象,通过线程对象来开启我们的线程。代理
        //Thread thread = new Thread(testThread03);
        //thread.start();
        //上面两行代码可以合成一个
        new Thread(testThread03).start();
        for (int i = 0; i < 1000; i++) {
            System.out.println("主线程:"+i);
        }
    }
}

上述代码段中使用的构造方法是Thread((Runnable target, String name),Runnable是一个接口类,方法体只包含一个抽象方法run。既然参数传进来的是接口类,那么也可以使用匿名内部类(如果线程只使用一次):

public class testRunnable {
    public static void main(String[] args) {
        //创建可运行对象
        Runnable runnable=new Runnable() {  
            @Override
            public void run() {
                for(int i=0;i<10;i++) {
                    System.out.println("子线程:"+i);
                }
            }
        };
        //创建线程对象
        Thread thread=new Thread(runnable, "子线程");
        thread.start();
        for(int i=0;i<10;i++) {
            System.out.println("主线程:"+i);
        }
    }
}

5. Runnable小案例

  1. 实现四个窗口共卖100张票
/**
 * 票类,实现买票功能
 */
public class Ticket implements Runnable{
    int ticket=100;
    @Override
    public void run() {
        while(ticket>0) {
            System.out.println(Thread.currentThread().getName()+"卖出了一张票,还剩"+(--ticket)+"张。");
        }
    }
}
public class testTicket {
    public static void main(String[] args) {
        //创建票对象
        Ticket ticket=new Ticket();
        //创建线程对象
        Thread w1=new Thread(ticket,"窗口1");
        Thread w2=new Thread(ticket,"窗口2");
        Thread w3=new Thread(ticket,"窗口3");
        Thread w4=new Thread(ticket,"窗口4");
        w1.start();
        w2.start();
        w3.start();
        w4.start();
    }
}

以这样的逻辑写代码是没有错的,但是在运行的时候大家会发现控制台打印的似乎并没有实现“共享”,但最终都会有某一个窗口卖完票:

窗口1卖出了一张票,还剩99张。
窗口4卖出了一张票,还剩96张。
窗口3卖出了一张票,还剩97张。
窗口2卖出了一张票,还剩98张。
窗口3卖出了一张R票,还剩93张。
窗口4卖出了一张票,还剩94张。
窗口1卖出了一张票,还剩95张。
窗口1卖出了一张票,还剩89张。
    ......
窗口4卖出了一张票,还剩2张。
窗口4卖出了一张票,还剩1张。
窗口4卖出了一张票,还剩0张。
窗口3卖出了一张票,还剩16张。
窗口2卖出了一张票,还剩17张。

这是因为线程是抢夺式占用CPU,每个线程都以各自的不可预知的进度执行。等后面讲完线程的同步之后你可以再来理解这个案例。

  1. 今天是月初你爸往你银行卡存钱同时你从卡里取钱使用程序模拟这个过程
/**
 * 银行卡(普通类)
 */
public class BandCard {
    private int Money;
    public int getMoney() {
        return Money;
    }
    public void setMoney(int money) {
        Money = money;
    }   
}
/**
 * 存钱功能(功能类)
 */
public class AddMoney implements Runnable{
    BandCard card;
    public AddMoney(BandCard bandCard) {
        card=bandCard;
    }
    @Override
    public void run() {
        //存10次
        for(int i=0;i<10;i++) {
            //往卡里存200
            card.setMoney(card.getMoney()+200);
            System.out.println(Thread.currentThread().getName()+"存了200元,卡里余额为:"+(card.getMoney()));
        }
    }
}
/**
 * 取钱功能
 */
public class SubMoney implements Runnable{
    BandCard card;
    public SubMoney(BandCard bandCard) {
        card=bandCard;
    }
    @Override
    public void run() {
        for(int i=0;i<10;i++) {
            if(card.getMoney()>=200) {
                //往卡里取200
                card.setMoney(card.getMoney()-200);
                System.out.println(Thread.currentThread().getName()+"取了200元,卡里还剩"+card.getMoney());
            }else {
                //余额不足,回退这次取钱过程,否则有效的取钱次数可能不到10次
                i--;
                System.out.println("余额不足");
            }
        }
    }
}
public class testBankCard {
    public static void main(String[] args) {
        //创建银行卡对象
        BandCard bandCard=new BandCard();
        //创建功能对象
        AddMoney addMoney=new AddMoney(bandCard);
        SubMoney subMoney=new SubMoney(bandCard);
        //创建线程对象并启动
        new Thread(addMoney,"爸爸").start();
        new Thread(subMoney,"我").start();
    }
}

运行代码结果如下:

爸爸存了200元,卡里余额为:200
我取了200元,卡里还剩0
余额不足
余额不足
余额不足
余额不足
余额不足
爸爸存了200元,卡里余额为:200
爸爸存了200元,卡里余额为:400
爸爸存了200元,卡里余额为:600
爸爸存了200元,卡里余额为:800
爸爸存了200元,卡里余额为:1000
爸爸存了200元,卡里余额为:1000
爸爸存了200元,卡里余额为:1200
爸爸存了200元,卡里余额为:1400
爸爸存了200元,卡里余额为:1600
我取了200元,卡里还剩800
我取了200元,卡里还剩1400
我取了200元,卡里还剩1200
我取了200元,卡里还剩1000
我取了200元,卡里还剩800
我取了200元,卡里还剩600
我取了200元,卡里还剩400
我取了200元,卡里还剩200
我取了200元,卡里还剩0

当然每次运行结果是不一样的,而且可以注意到控制台打印的数据似乎并不“正确”,原因同上一个案例,不过最终的结果卡里还是0元。这个案例也可以写成匿名内部类以减少代码量,缺点是可读性差,这里不再演示。

6. 创建线程(三)

实现Callable接口创建线程(了解)

  1. 实现Callable接口,需要返回值类型
  2. 重写call方法,不要抛出异常
  3. 创建目标对象
  4. 创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(3);
  5. 提交执行:Future<Boolean> result1 = ser.submit(t1);
  6. 获取结果:boolean r1 = result.get();
  7. 关闭服务:ser.shutdownNow();
//下载图片案例修改为Callable
package com.gong.Demo01;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.*;

public class MyCallable07 implements Callable<Boolean> {
    private String url;
    private String name;

    public MyCallable07(String url, String name) {
        this.url = url;
        this.name = name;
    }

    @Override
    public Boolean call() {
        WebDownLoader webDownLoader = new WebDownLoader();
        webDownLoader.downLoader(url, name);
        System.out.println("下载了文件名为:"+name);
        return true;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        MyCallable07 t1 = new MyCallable07("http://image.qianye88.com/pic/100dc4c31a4cfa4c6d3def642fb2efaf?imageMogr2/thumbnail/x280/quality/90!","1.jpg");
        MyCallable07 t2 = new MyCallable07("http://image.qianye88.com/pic/fb803f9e848476306bac335c1073e0a0?imageMogr2/thumbnail/x280/quality/90!","3.jpg");
        MyCallable07 t3 = new MyCallable07("http://image.qianye88.com/pic/28f7f2ed45594a7952b9511a0fcd3fc1?imageMogr2/thumbnail/x280/quality/90!","2.jpg");
        //1. 创建执行服务:
        ExecutorService ser = Executors.newFixedThreadPool(6);
        //2提交执行:
        Future<Boolean> result1 = ser.submit(t1);
        Future<Boolean> result2 = ser.submit(t2);
        Future<Boolean> result3 = ser.submit(t3);
        //3. 获取结果:
        boolean r1 = result1.get();
        boolean r2 = result1.get();
        boolean r3 = result1.get();
        //4. 关闭服务:
        ser.shutdownNow();
    }
}
class WebDownLoader01 {
    public void downLoader(String url,String name){
        try {
            FileUtils.copyURLToFile(new URL(url), new File(name));
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("downLoader异常");
        }
    }
}

小结

  • 继承Thread类
    • 子类继承Thread类具备多线程能力
    • 启动线程:子类对象.start()
    • 不建议使用:避免OOP单继承局限性
  • 实现Runnable接口
    • 实现接口Runnable具有多线程能力
    • 启动线程:new Thread(启动线程目标).start
    • 推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用

并发问题从上面Runnable卖票代码我们可以看到多个对象同时操作一个资源的时候线程不安全了,数据紊乱

静态代理模式总结

  • 真实对象和代理对象都要实现用一个接口
  • 代理对象要代理真实对象
  • 好处
    • 代理对象可以做很多真实对象做不了的事情(增加功能)
    • 真实对象专注自己的事情
  • Thread()就是代理对象

三、 线程的基本状态

1.五大状态

线程的基本状态可以分为:

  1. 初始状态

    当线程对象被创建(new)之后即为初始状态。

  2. 就绪状态

    线程对象调用start方法之后进入就绪状态,此时只要获得了处理器便可以立即执行。

  3. 运行状态

    获得处理器之后,则进入运行状态,直到所分配的时间片结束,然后继续进入就绪状态。

  4. 等待状态

    因为发生某种事情而无法继续执行下去,例如调用sleep方法时线程进入限期等待,因某线程调用join使当前线程进入无限期等待。下一节会提到这两个方法。

  5. 终止状态

    主线程(main)结束或者该线程的run方法结束则进入终止状态,并释放CPU。

线程状态1

线程状态2

线程状态3

2. 状态检测

在JDK1.5之后,把就绪状态和运行状态合成了一个Runnable状态,可以通过public Thread.State getState()方法获取当前线程的状态。

我们可以通过源码来查看一下这几个状态:

public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}

这个State返回类型实际上一个枚举类。

  • NEW 初始状态

    线程还没有启动时的状态。

  • RUNNABLE 就绪状态和执行状态

    线程启动时的状态。线程被JVM所执行但它还可能需要一些来自操作系统的其他资源才能执行。

  • BLOCKED 阻塞状态

    线程被一个监听锁所阻塞时的状态。

  • WAITING (无期限)等待状态

    线程正在等待时的状态。线程被以下方法所调用就会进入等待状态:

    • Object.wait无参方法
    • Thread.join无参方法
    • LockSupport.park

    wait方法可以让当前线程进入等待状态,需要其他线程调用此线程对象的notify方法或者notifyAll方法来唤醒此线程;调用join方法的线程需要等到被调用线程终止才能结束等待状态。

  • TIMED_WAITING 有限等待状态

    线程在指定时间后才能结束等待的一种等待状态。是由于调用了以下方法所引起的一种状态:

    • Thread.sleep
    • Object.wait带参方法
    • Thread.join带参方法
    • LockSupport.parkNanos
    • LockSupport.parkUntil
  • TERMINATED 终止状态

    线程终止时的状态。该线程已经执行完毕。


线程状态

  • Thread.getState();
  • 线程死亡后不可以重新start()
package com.gong.Demo02;

public class TestState {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("线程执行了");
        });
        Thread.State state = t1.getState();//启动钱前线程状态new
        System.out.println(state);
        t1.start();
        state = t1.getState();//启动后状态RUNNABLE
        System.out.println(state);
        while(state!=Thread.State.TERMINATED){
            Thread.sleep(100);
            System.out.println(state);
            state = t1.getState();
        }
        System.out.println(state);
        //线程死亡后不可以重新start()

    }
}

四、线程常用方法

方法 说明
setPriority(int newPriority) 更改线程优先级
Static void sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠
Void join() 等待该线程终止
Static void yield() 暂停当前正在执行的线程对象,并执行其他线程
Void interrupt() 中断线程,别用这个方式
Boolean isAlive() 测试线程是否处于活动状态

1.停止线程

  • 不推荐使用JDK提供的stop()、destroy()方法【已废弃】
  • 推荐线程自己停下来
  • 建议使用一个标志位进行终止变量,当flag = false,则终止线程运行。

利用标志位暂停线程案例:

package com.gong.Demo02;

import sun.lwawt.macosx.CPrinterDevice;

public class TestStop implements Runnable {
    boolean flag = true;//利用标志位停止线程
    @Override
    public void run() {
        int i = 0;
        while(flag){
            System.out.println(Thread.currentThread().getName()+"运行了-------------------------"+i++);
        }

    }
    //自己写停止方法
    public void stop(){
        flag = false;
    }
    public static void main(String[] args) {

        TestStop t1 = new TestStop();
        new Thread(t1,"子线程").start();
        for (int i = 0; i < 500; i++) {
            System.out.println("主线程跑了"+i+"次");
            if (i==100){
                t1.stop();
                System.out.println("子线程结束了");
            }
        }
    }
}

2.线程休眠

  • sleep(时间)指当前线程阻塞的毫秒数

  • sleep存在异常Interruptedexception

  • sleep时间达到后线程进入就绪状态

  • sleep可以模拟网络延时,倒计时等。

  • 每一个对象都有一个锁,sleep不会释放锁

  • public static void sleep(long millis)

    当前线程主动休眠millis毫秒。

/**
 * 演示sleep的使用,前面抢票也用过延时
 */
public class test {
    public static void main(String[] args) throws InterruptedException {
        for(int i=10;i>=0;i--) {
            System.out.println(i);
            //(主线程)每隔一秒打印一次
            Thread.sleep(1000);
        }
    }
}
package com.gong.Demo02;
//显示当前时间

import java.text.SimpleDateFormat;
import java.util.Date;

public class TestSleep {
    public static void main(String[] args) {
        Date nowTime = new Date(System.currentTimeMillis());
        while(true){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(new SimpleDateFormat("YYYY-MM-dd HH:mm:ss").format(nowTime));
            nowTime = new Date(System.currentTimeMillis());
        }
    }
}

3. 线程礼让

  • 礼让线程,让当前正在执行的线程暂停,但不阻塞

  • 将线程从运行状态转为就绪状态

  • 让CPU重新调度,礼让不一定成功,看CPU心情

  • public static void yield()

    当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片。

package com.gong.Demo02;
/**
 * 演示yield的使用
 */
public class TestYield implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"线程开始了");
        Thread.yield();
        System.out.println(Thread.currentThread().getName()+"线程结束了");
    }

    public static void main(String[] args) {
        TestYield testYield = new TestYield();
        new Thread(testYield,"a").start();
        new Thread(testYield,"b").start();
    }
}

在测试类里创建两个线程对象执行上述代码,所得到的打印结果会更接近于交替打印。

4.线程插队

  • Join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞
  • 可以想象成阻塞
  • public final void join()

允许其他线程加入到当前线程中。当某线程调用该方法时,加入并阻塞当前线程,直到加入的线程执行完毕,当前线程才继续执行。

/**
 * 演示join的使用
 */
public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for(int i=0;i<10;i++) {
            System.out.println(Thread.currentThread().getName()+":"+i);           
        }
    }
}
public class test {
    public static void main(String[] args) throws InterruptedException{
        MyRunnable myRunnable=new MyRunnable();
        Thread thread=new Thread(myRunnable,"子线程");
        thread.start();
        //加入到当前线程(主线程main),并阻塞当前线程
        //必须要在线程启动之后调用
        thread.join();
        for(int i=0;i<10;i++) {
            System.out.println(i);
        }
    }
}

注释掉join这行代码,就和之前运行的结果一样,两个线程抢占执行;调用join之后结果如下:

子线程:0
子线程:1
子线程:2
子线程:3
子线程:4
子线程:5
子线程:6
子线程:7
子线程:8
子线程:9
0
1
2
3
4
5
6
7
8
9

子线程加入到主线程并阻塞了主线程,子线程执行完毕后才恢复主线程的运行。

5.线程优先级

  • Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行

  • 线程的优先级用数字表示,范围1~10

    • Thread.MIN_PRIORITY =1;
    • Thread.NORM_PRIORITY =5;
    • Thread.MAX_PRIORITY =10;
  • 优先级越高表示获取CPU机会越多

  • 使用以下方法获取或改变优先级

    • getPriority();
    • setPriority(int newPriority);

注:线程设置优先级要在start()之前

package com.gong.Demo02;

public class TestPrioriy {
    public static void main(String[] args) {
        MyPriority myPriority1 = new MyPriority();
        Thread thread1 = new Thread(myPriority1, "线程1");
        Thread thread2 = new Thread(myPriority1, "线程2");
        Thread thread3 = new Thread(myPriority1, "线程3");
        Thread thread4 = new Thread(myPriority1, "线程4");
        Thread thread5 = new Thread(myPriority1, "线程5");
        thread1.setPriority(1);
        thread1.start();
        System.out.println(thread1.getPriority());

        thread2.setPriority(10);
        thread2.start();
        System.out.println(thread2.getPriority());

        thread3.setPriority(10);
        thread3.start();
        System.out.println(thread3.getPriority());

        thread4.setPriority(10);
        thread4.start();
        System.out.println(thread4.getPriority());

        thread5.setPriority(10);
        thread5.start();
        System.out.println(thread5.getPriority());
    }
}
class MyPriority implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"执行了");
    }
}

6.守护线程

  • 线程分为用户线程和守护线程

  • 虚拟机必须确保用户线程执行完毕

  • 虚拟机不用等待守护线程执行完毕

  • 如后台记录操作日志,监控内存,垃圾回收等等

  • public final void setDaemon(boolean on)

    如果参数为true,则标记该线程为守护线程。

    在JAVA中线程有两类:用户线程(前台线程)、守护线程(后台线程)。守护可以理解为守护用户线程。如果程序中所有用户线程都执行完毕了,守护线程会自动结束。垃圾回收线程属于守护线程

/**
 * 演示守护线程
 */
public class MyThread extends Thread{
    public MyThread() {     
    }
    public MyThread(String name) {e

        super(name);
    }
    @Override
    public void run() {
        for(int i=0;i<50;i++) {
            System.out.println(Thread.currentThread().getName()+":"+i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class test {
    public static void main(String[] args) throws InterruptedException{
        MyThread thread=new MyThread();
        //必须在start之前设置
        thread.setDaemon(true);
        thread.start();
        for(int i=0;i<10;i++) {
            System.out.println(i);
            Thread.sleep(200);
        }
    }
}

我们知道线程争夺的情况,但当某个线程被设置成守护线程时,结果如下:

0
Thread-0:0
1
2
Thread-0:1
3
4
Thread-0:2
5
6
7
Thread-0:3
8
9
Thread-0:4

当主线程执行完毕后,子线程只打印了4次,但因为前者的结束而结束。


五、线程(同步)安全

  • 并发:同一个对象被多个线程同时操作(如买票)

  • 处理多线程问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象,这时候我们就需要线程同步,线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用

  • 线程同步形成条件:队列+锁

由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入**锁机制**synchronized ,当一个线程获得对象的排它锁,独占资源,其他线程必须等待,使用后释放锁即可.存在以下问题:

  • 一个线程持有锁会导致其他所有需要此锁的线程挂起;

  • 在多线程竞争下,加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题;

  • 如果一个优先级高的线程等待一个优先级低的线程释放锁 会导致优先级倒置,引起性能问题.

多线程安全问题

  • 当多线程并发访问临界资源时,如果破坏了原子操作,可能会造成数据不一致。

    • 临界资源:共享资源(对于同一个对象),一次仅允许一个线程使用,才可以保证其正确性。
  • 原子操作:不可分割的多步操作,被视为一个整体,其顺序和步骤不可打乱或缺省,比如上一段代码的存hello和存world应当被看成两个原子操作。

补充:

临界资源和临界区(针对线程而言):

线程在运行过程中,会与同一进程内的其他线程共享资源,把同时只允许一个线程使用的资源称为临界资源。为了保证临界资源的正确使用,可以把临界资源的访问分成以下四个部分:

  1. 进入区。为了进入临界区使用临界资源,在进入区要检查是否可以进入临界区;如果可以进入临界区,通常设置相应的“正在访问临界区”标志,以阻止其他线程同时进入临界区。
  2. 临界区线程用于访问临界资源的代码又称临界段
  3. 退出区。临界区后用于将“正在访问临界区”标志清除部分。
  4. 剩余区。线程中除上述3部分以外的其他部分。

简单来说,临界资源是一种系统资源,需要不同的线程互斥访问,例如前文代码中的数组;而临界区则是每个线程中访问临界资源的一段代码,是属于对应线程的,临界区前后需要设置进入区和退出区以进行检查和恢复。

JAVA中,在程序应用里要保证线程的安全性就需要用到同步代码块

1.同步方法

  • 同步方法
    • 由于我们可以通过private关键字来保证数据对象只能被方法访问, 所以我们只需要针对方法提出一套机制,这套机制就是synchronized关键字,它包括两种用法:synchronized方法synchronized块.
      同步方法: public synchronized void method(int args) {}
    • synchronized方法控制对 “对象”的访问,每个对象对应一把锁 ,每个synchronized方法都必须获得调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行

缺陷:若将一个大的方法申明为synchronized将会影响效率,方法里面需要修改的内容才需要锁,所得太多,浪费资源

//对当前对象(this)加锁
synchronized 返回值类型 方法名称(形参列表){
    //代码(原子操作)
}
//简单的说就是在方法前加修饰符synchronized

利用同步方法解决售票问题

package com.gong.Demo03;
//模拟自助售票
public class NotSafeTicket {

    public static void main(String[] args) {
        Ticket t1 = new Ticket();
        new Thread(t1,"我").start();
        new Thread(t1,"你").start();
        new Thread(t1,"他").start();

    }
}

class Ticket implements Runnable{
    int ticketnums = 10;
    boolean flag = true;
    @Override
    public void run() {

        while(flag){
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            buy();

        }

    }
    public synchronized void buy()  {
        if (ticketnums <= 0){
            flag = false;
            System.out.println("已经没票了");
            return;
        }
        System.out.println(Thread.currentThread().getName()+"买了第"+ticketnums--+"张票");

    }
}

2.同步块

  • 同步块: synchronized (Obj ){}
  • Obj 称之为同步监视器
    • Obj可以是任何对象,但是推荐使用共享资源作为同步监视器
    • 同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this ,就是这个对象本身,或者是class [反射中讲解]
  • 同步监视器的执行过程
    • 第一个线程访问,锁定同步监视器,执行其中代码.
    • 第二个线程访问 ,发现同步监视器被锁定,无法访问.
    • 第一个线程访问完毕,解锁同步监视器.
    • 第二个线程访问, 发现同步监视器没有锁,然后锁定并访问双步老的

代码块

//对临界资源对象加锁
synchronized(临界资源对象){
    //代码块(原子操作)
}
//就是给多个线程同时操作的变量加锁,临界资源对象=操作的变量

现在就能解决之前的卖票小案例,并使用同步代码块来实现互斥访问票这个临界资源

package com.gong.Demo03;
//模拟银行取钱
public class NotSafeBank {
    public static void main(String[] args) {
        //创建一个共同账户
        Account account = new Account("小金库",10000);
        Bank xiaoming = new Bank(account, 500, "小明");
        Bank xiaohong = new Bank(account, 1000, "小红");
        xiaoming.start();
        xiaohong.start();
    }
}
class Account{
    String name;//账户名
    int money;//账户余额
    public Account(String name, int money) {
        this.name = name;
        this.money = money;
    }
}
//模拟银行取钱
class Bank extends Thread{
    Account account;//账户
    int drawingMoney;//取多少钱
    int nowMoney;//现在多钱

    public Bank(Account account, int drawingMoney, String name) {
        super(name);
        this.account = account;
        this.drawingMoney = drawingMoney;
    }

    @Override
    public void run() {
//方法块方法块方法块方法块方法块方法块
        synchronized (account){
            if (account.money-drawingMoney<0){
                System.out.println(this.getName()+"您好,当前余额不足,失败!");
                return;
            }
            //模拟延时让两个取钱人都看到有余额即上一步满足
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //手里的钱
            nowMoney = nowMoney+drawingMoney;
            //账户余额
            account.money = account.money-drawingMoney;
            System.out.println(this.getName()+"取了"+drawingMoney);
            System.out.println(this.getName()+"手里有:"+nowMoney);
            System.out.println(account.name+"余额:"+account.money);
        }
    }
}

再次运行你就能看到期望的结果,这里不再演示。

注:

每个对象都有一个互斥锁标记用来分配给线程的

只有拥有对象互斥锁标记的线程,才能进入对该对象加锁的同步代码块。

线程退出同步代码块时,会释放相应的互斥锁标记。

  • 线程的状态阻塞

    当线程访问临界区(同步块代码)时,如果没有拿到访问锁,便进入阻塞状态。

注:

只有拥有对象互斥锁标记的线程,才能进入该对象加锁的同步方法中。线程退出同步方法时,会释放相应的互斥锁标记。

  • 同步规则

    • 只有在调用包含同步代码块的方法,或者同步方法时,才需要对象的锁标记。

    临界区(互斥执行)才需要加锁。

    • 如调用不包含同步代码块的方法,或普通方法时,则不需要锁标记,可直接调用。

    • 已知JDK中线程安全的类:

    • StringBuffer

    • Vector

    • Hashtable

    • 以上类中的公开方法,均为synchronized修饰的同步方法。

3. 经典问题(死锁)

死锁

  • 当第一个线程拥有A对象锁标记,并等待B对象锁标记,同时第二个线程拥有B对象锁标记,并等待A对象锁标记时,产生死锁。
  • 一个线程可以同时拥有多个对象的锁标记,当线程阻塞时,不会释放已经拥有的锁标记,由此可能造成死锁。

简单的说就是,多个线程各自占有一些资源,并且互相等待其他线程占有的资源才能运行,从而导致两个或多个线程都在等待对方释放资源,都停止的情形,某一个同步块同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。

补充:

死锁的概念(对于线程):

当多个线程因竞争系统资源或互相通信而处于半永久阻塞状态时,若无外力作用,这些线程都将无法向前推进。这些线程中的每一个线程,均无限期地等待此组线程中某个其他线程占用的、自己永远无法得到的资源,这种现象称为死锁。

资源分类

现代操作系统所管理的资源类型十分丰富,并且可以从不同角度出发对其进行分类,例如,可以把资源分为可剥夺资源和不可剥夺资源。

  • 可剥夺资源是指虽然资源占有者线程需要使用该资源,但另一个线程可以强行把该资源从占有者线程处剥夺过来自己使用。
  • 不可剥夺资源是指除非占有者线程不再需要使用该资源而主动释放资源,否则其他线程不得在占有者线程使用资源过程中强行剥夺。

死锁产生的原因是竞争资源。可剥夺资源的竞争不会引起死锁。更进一步看,死锁产生的原因是系统资源不足和线程推进顺序不当;后者是重要原因而前者是根本原因。

死锁避免的方法

产生死锁的四个必要条件

  • 户斥条件:一个资源每次只能被一个进程使用
  • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺
  • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系

上面列出了死锁的四个必要条件,我们要想办法破其中的任意一个或多个条件就可以避免死锁发生

通过一个小案例来演示死锁的产生,假如两个人A和B在桌子上同时吃饭,桌上只有一双筷子,当一个人拥有两根筷子的时候才能吃:

package com.gong.Demo03;

public class TestDeadLock {
    public static void main(String[] args) {
        //创建两个锁对象,俩筷子
        Chopsticks chopsticks1 = new Chopsticks();
        Chopsticks chopsticks2 = new Chopsticks();
        //线程1
        Runnable A = new Runnable() {
            @Override
            public void run() {
                synchronized (chopsticks1){
                    System.out.println("A拿到第一根筷子");
                    synchronized (chopsticks2){
                        System.out.println("A拿到两根筷子,可以干饭");
                    }
                }
            }
        };
        Runnable B = new Runnable() {
            @Override
            public void run() {
                synchronized (chopsticks2){
                    System.out.println("B拿到第一根筷子");
                    synchronized (chopsticks1){
                        System.out.println("B拿到两根筷子,可以干饭");
                    }
                }
            }
        };
        new Thread(A).start();
        new Thread(B).start();
    }
}
//一个筷子
class Chopsticks{

}

运行之后程序进入死锁状态,并且无限期地等待下去:

//控 制台打印(程序未结束)
B拿到了一根筷子。
A拿到了一根筷子。
//解决方法将内部的synchronized放到外面
//或者其中一个加一个延时

A和B各持有一根筷子,并且都在等待对方的一根筷子,导致两个人都吃不了饭。可以通过sleep方式使其中一个线程休眠一小会,A(B)吃完B(A)再吃;或者把A(B)同步代码块中的锁换一下位置,一开始两个人都抢同一根筷子,没抢到的就等另一个吃完饭。

5. 线程通信

通信方法

  • 等待:

    • public final void wait()表示线程一直等待,知道其他线程同志,与sleep不同,会释放锁
    • public final void wait(long timeout)指定等待毫秒数
    • 必须在对obj加锁的同步代码块中调用。在一个线程中,调用obj.wait()时,此线程会释放其拥有的所有锁标记。同时此线程阻塞在obj的等待队列中。总而言之,就是释放锁,进入等待队列。
  • 通知:

    • public final void notify()唤醒一个处于等待状态的线程
    • public final void notifyAll()唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先调度
    • 进入等待的线程需要其他线程调用该线程的通知方法来将其唤醒。

解决方式

解决方式1:并发协作模型“生产者/消费者模式”-->管程法

  • 生产者:负责生产数据的模块(可能是方法,对象,线程,进程);
  • 消费者:负责处理数据的模块(可能是方法,对象,线程, 进程);
  • 缓冲区:消费者不能直接使用生产者的数据,他们之间有个“缓冲区

生产者将生产好的数据放入缓冲区,消费者从缓冲区拿出数据

解决方法2:并发协作模型“生产者/消费者模式”-->信号灯法

  • 设置一个flag标签判定什么时候等待什么时候通知

6. 经典问题(生产者消费者)

若干个生产者在生产产品,这些产品将提供给若干个消费者去消费,为了使生产者和消费者能并发执行,在两者之间设置一个能存储多个产品的缓冲区,生产者将生产的产品放入缓冲区中,消费者从缓冲区取走产品进行消费,显然生产者和消费者之间必须保持同步,即不允许消费者到一个空的缓冲区中取产品,也不允许生产者向一个满的缓冲区中放入产品。

管程法

利用生产食物和消费食物案例,用线程通信再来演示以下是演示代码:

package com.gong.Demo03;

import java.beans.FeatureDescriptor;

public class TestPC {
    public static void main(String[] args) {
        Buffer container = new Buffer();
        Producter producter = new Producter(container);
        Consummer consummer = new Consummer(container);
        producter.start();
        consummer.start();
    }
}
//生产者
class Producter extends Thread{
    Buffer container;
    public Producter(Buffer container) {
        this.container = container;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("生产了"+i+"个食物");
                container.push(new Food(i));
        }
    }
}
//产品
class Food{
    int id;
    public Food(int id) {
        this.id = id;
    }
}
//消费者
class Consummer extends Thread{
    Buffer container;
    public Consummer(Buffer container) {
        this.container = container;
    }
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("吃了"+container.pop().id+"个食物");

        }
    }
}
//缓冲区
class Buffer {
    Food[] foodbuffer = new Food[10];
    int count = 0;
    //生产者放入产品
    public synchronized void push(Food food) {
        if( count == foodbuffer.length){
            //如果满了通知消费者消费,生产等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //如果没满添加产品
        foodbuffer[count] = food;
        count++;
        this.notifyAll();
    }

    //消费者消费产品
    public synchronized Food pop(){
        if (count == 0){
            //没鸡消费者等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        count--;
        Food food = foodbuffer[count];
        this.notifyAll();
        return food;
    }
}

信号灯法

我们知道,我们过马路的时候是需要看信号灯的(可能有人不看)当绿灯亮的时候,行人可以走,红灯亮的时候,行人不可以走,车子便可以走了,那么同理,信号灯法就是这个意思,我们需要定一个变量来做信号灯,这里推荐定义Boolean,那么话不多说,我们直接上代码,为了方便大家理解,我们把对象写成,表演者,观看者,表演者拍完后观众看

package com.gong.Demo03;

public class TestPC2 {
    public static void main(String[] args) {
        Program program = new Program();
        Player player = new Player(program);
        Audiance audiance = new Audiance(program);
        player.start();
        audiance.start();

    }
}
//生产者->演员
class Player extends Thread{
    Program program;
    public Player(Program program){
        this.program = program;
    }
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            if ( i%2==0 ){
                try {
                    this.program.player("表演了节目"+i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }else {
                try {
                    this.program.player("广告"+i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
//消费者->听众
class Audiance extends Thread{
    Program program ;
    public Audiance(Program program){
        this.program = program;
    }
    @Override
    public void run() {

        for (int i = 0; i < 20; i++) {
            try {
                this.program.audiance();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
//产品->表演
class Program{
    String program;//表演的节目
    boolean flag = true;
    //演员表演时观众等待
    //观众观看时演员等待

    //表演
    public synchronized void player(String program) throws InterruptedException {
        if (!flag){
            this.wait();
        }
        this.program = program;
        this.flag = !this.flag;
        System.out.println("表演了"+program);
        //通知
        this.notifyAll();//唤醒等待
    }
    //观看
    public synchronized void audiance() throws InterruptedException {
        if (flag == true){
            this.wait();
        }
        System.out.println("观众观看完了"+program);
        this.flag = !this.flag;
        this.notifyAll();
    }
}

六、Lock接口

  • JDK1.5加入,与synchronized比较,不仅显示定义,而且结构更灵活。
  • 提供了更多实用性方法,功能更强大、性能更优越。

常用方法:

  • void lock

    获取锁,如果锁被占用,当前线程则进入等待状态。

  • boolean tryLock()

    尝试获取锁(成功返回true,失败返回false,不阻塞)

  • void unlock()

    释放锁。

1. 可重入锁

  • ReentrantLock: Lock接口的实现类,与synchronized一样具有互斥锁功能。

    所谓重入锁,是指一个线程拿到该锁后还可以再次成功获取,而不会因为该锁已经被持有(尽管是自己所持有)而陷入等待状态(死锁)。之前说过的synchronized也是可重入锁

1.2 重入锁的使用

还是以卖票案例为例进行演示。

//重入锁的使用
public class Ticket implements Runnable{
    int ticket=100;
    //创建重入锁对象
    ReentrantLock lock=new ReentrantLock();
    @Override
    public void run() { 
        while(true) {   
            //上锁        
            lock.lock();
            try {                   
                if(ticket>0)
                    System.out.println(Thread.currentThread().getName()+"卖出了一张票,还剩"+(--ticket)+"张。");
                else break;
            } finally {
                //解锁
                lock.unlock();
            }   
        }
    }
}

这里主要注意一下上锁后记得解锁,有几个lock就要有对应的几个unlock。

2.Lock和synchronized的对比

  • Lock是显示锁(手动开启关闭,别忘记关锁)synchronized是隐式锁。出了作用与自动释放
  • Lock只有代码块锁,synchronized有代码块锁和方法锁
  • 使用Lock锁,JVM将话费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
  • 优先顺序
    • Lock > 同步代码块(已经进入方法体,分配了相应资源) > 同步方法(在方法体之外)

3. 读写锁

ReentrantReadWriteLock:

  • 一种支持一写多读的同步锁,读写分离,可以分别分配读锁和写锁。
  • 支持多次分配读锁,使多个读操作可以并发执行。

互斥规则:

  • 写—-写:互斥,一个线程在写的同时其他线程会被阻塞。
  • 读—-写:互斥,读的时候不能写,写的时候不能读。
  • 读—-读:不互斥、不阻塞。
  • 在读操作远远高于写操作的环境中,可在保证线程安全的情况下,提高运行效率。
//演示读写锁的使用
public class ReadWriteLock {
        //创建读写锁对象
        ReentrantReadWriteLock rrlLock=new ReentrantReadWriteLock();
        ReadLock readLock=rrlLock.readLock();//获得读锁
        WriteLock writeLock=rrlLock.writeLock();//获得写锁
        private int value=999;
        //读方法
        public int getValue() {
            readLock.lock();//开启读锁
            try {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return this.value;
            } finally {
                readLock.unlock();//释放读锁
            }           
        }
        //写方法
        public void setValue(int value) {
            writeLock.lock();//开启写锁
            try {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                this.value=value;
            } finally {
                writeLock.unlock();//释放写锁
            }   
        }
}
public class testReadWriteLock {
    public static void main(String[] args) {
        ExecutorService eService=Executors.newFixedThreadPool(20);
        ReadWriteLock rwlLock=new ReadWriteLock();
        Runnable read=new Runnable() {      
            @Override
            public void run() {
                System.out.println(rwlLock.getValue());
            }
        };
        Runnable write=new Runnable() {     
            @Override
            public void run() {
                rwlLock.setValue(666);
                System.out.println("改写为666");
            }
        };
        //写2次
        for(int i=0;i<2;i++) {
            eService.submit(write);
        }
        //读18次
        for(int i=0;i<18;i++) {
            eService.submit(read);
        }   
        eService.shutdown();
    }
}

通过调用sleep可以观察到,只有在读写交替和两个写操作的时候程序是互斥执行,而在读操作时线程之间是并发执行。

给TA打赏
共{{data.count}}人
人已打赏
JavaGUIJava笔记

Java——实现贪吃蛇GUI

2021-9-13 0:28:33

Java笔记多线程

Java多线程(二)

2021-9-13 11:28:46

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
购物车
优惠劵
有新私信 私信列表
搜索