笔记部分整理自 尚学堂.百战程序员 视频链接

进程和线程的区别

  • 线程在进程中运行的。
  • 一个进程可以包含多个线程。
  • 不同进程间数据很难共享,而同一进程下不同线程间数据很易共享。
  • 进程要比线程消耗更多的计算机资源。
  • 进程间不会相互影响,因为它们的空间是完全隔离的。而进程中的一个线程挂掉将导致整个进程挂掉。
  • 进程使用的内存地址可以上锁,即一个线程使用某些共享内存时,其他线程必须等待结束才能使用这一块内存。

线程的创建

通过继承Thread类实现多线程

在Java中负责实现线程功能的类是java.lang.Thread 类。
继承Thread类实现多线程的步骤:

  1. 继承Thread类定义线程类。
  2. 重写Thread类中的run()方法。run()方法也称为线程体。
  3. 实例化线程类并通过start()方法启动线程。

子线程要在主线程中启动

public class TestThread extends Thread{
    public static void main(String[] args){
        System.out.println("主线程开始");
        //实例化线程类
        TestThread t1 = new TestThread();
        TestThread t2 = new TestThread();
        //启动线程
        //如果直接写t1.run(); 那么就在主线程中调用run方法
        //所以我们需要使用start方法启动
        t1.start();
        t2.start();    //两个线程并发
        //此时有三个线程:主线程和两个子线程
        //程序运行->实例化线程类->子线程启动->主线程结束->子线程工作->程序结束
        System.out.println("主线程结束");
    }
    
    public TestThread(){
        System.out.println(this.getName());
    }
    
    //线程的线程体
    @Override
    public void run(){
        System.out.println(this.getName()+"线程开始");
        for(int i=0;i<20;i++){
            System.out.println(this.getName()+i);
        }
        System.out.println(this.getName()+"线程结束");
    }
    
}

通过实现Runnable接口实现多线程

public class TestThread implements Runnable{
    public static void main(String[] args){
        System.out.println("主线程开始");
        TestThread t1 = new TestThread();
        //启动线程
        Thread t = new Thread(t1);
        t.start();
        System.out.println("主线程结束");
    }
    public TestThread(){
        //这里会输出主线程名称
        System.out.println(Thread.currentThread().getName());
    }
    
    //线程的线程体
    @Override
    public void run(){
        System.out.println(Thread.currentThread().getName()+"线程开始");
        for(int i=0;i<20;i++ ){
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
        System.out.println(Thread.currentThread().getName()+"线程结束");
    }
}

线程的执行流程

image-20220923204932996
image-20220923204932996

线程的生命周期

image-20220923222647384
image-20220923222647384

一个线程对象在它的生命周期内,需要经历5个状态。

  • 新生状态(New)

用new关键字建立一个线程对象后,该线程对象就处于新生状态。处于新生状态的线程有自己的内存空间,通过调用start方法进入就绪状态。

  • 就绪状态(Runnable)

处于就绪状态的线程已经具备了运行条件,但是还没有被分配到CPU,处于“线程就绪队列”,等待系统为其分配CPU。就绪状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会进入执行状态。一旦获得CPU,线程就进入运行状态并自动调用自己的run方法。有四种原因会导致线程进入就绪状态:

  1. 新建线程:调用start()方法,进入就绪状态;
  2. 阻塞线程:阻塞解除,进入就绪状态;
  3. 运行线程:调用yield()方法,直接进入就绪状态;
  4. 运行线程:JVM将CPU资源从本线程切换到其他线程。
  • 运行状态(Running)

在运行状态的线程执行自己run方法中的代码,直到调用其他方法而终止或等待某资源而阻塞或完成任务而死亡。如果在给定的时间片没有执行结束,就会被系统给换下来回到就绪状态。也可能由于某些“导致阻塞的事件”而进入阻塞状态。

  • 阻塞状态(Blocked)

阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪)。有4种原因会导致阻塞:

  1. 执行sleep(int millisecond)方法,使当前线程休眠,进入阻塞状态。当指定的时间到了后,线程进入就绪状态。
  2. 执行wait()方法,使当前线程进入阻塞状态。当使用nofity()方法唤醒这个线程后,它进入就绪状态。
  3. 线程运行时,某个操作进入阻塞状态,比如执行IO流操作(read()/write()方法本身就是阻塞的方法)。只有当引起该操作阻塞的原因消失后,线程进入就绪状态。
  4. join()线程联合:当某个线程等待另一个线程执行结束后,才能继续执行时,使用join()方法。
  • 死亡状态(Terminated)

死亡状态是线程生命周期中的最后一个阶段。线程死亡的原因有两个。一个是正常运行的线程完成了它run()方法内的全部工作;另一个是线程被强制终止,如通过执行stop()destroy()方法来终止一个线程(注: stop()/destroy()方法已经被JDK废弃,不推荐使用)

当一个线程进入死亡状态后,就不能回到其他状态了。

线程的使用

终止线程

如果我们想在一个线程中终止另一个线程我们一般不使用JDK提供的stop()/destroy()方法(它们本身也被JDK废弃了)。通常的做是提供一个boolean型的终止变量,当这个变量值为false时,则终止线程的运行。

public class StopThread implements Runnable{
    public static void main(String[] args) throws Exception{
        System.out.println("主线程开始");
        StopThread st = new StopThread();
        Thread t1 = new Thread(st);
        t1.start();

        //使主线程阻塞,键盘输入后解除
        System.in.read();
        st.stop();
        System.out.println("主线程开始");
    }
    private boolean flag = true;
    @Override
    public void run(){
        System.out.println(Thread.currentThread().getName()+"线程开始");
        int i = 0;
        while(flag){
            System.out.println(Thread.currentThread().getName()+" "+i);
            try{
                Thread.sleep(200);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }

        //这个代码段使用的终止线程方法,会将下方的输出代码也执行
        System.out.println(Thread.currentThread().getName()+"线程结束");
    }
    public void stop(){
        this.flag = false;
    }
}

暂停线程

暂停当前线程执行sleep()/yield()

sleep()方法

暂停线程执行常用的方法有sleep()yield()方法,这两个方法的区别是:

  • sleep()方法:可以让正在运行的线程进入阻塞状态,直到休眠时间满了,进入就绪状态。
  • yield()方法:可以让正在运行的线程直接进入就绪状态,让出CPU的使用权。
public class SleepThread implements Runnable{
    public static void main(String[] args){
        System.out.println("主线程开始");
        Thread t = new Thread(new SleepThread());
        t.start();
        System.out.println("主线程结束");
    }
    @Override
    public void run(){
        System.out.println(Thread.currentThread().getName()+"线程开始");
        for(int i=0;i<20;i++){
            System.out.println(Thread.currentThread().getName());
            try{
                Thread.sleep(1000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName()+"线程结束");
    }
}

yeild()方法

yield()方法的作用:暂停当前正在执行的线程,并执行其他线程。
yield()让当前正在运行的线程回到可运行状态,以允许具有相同优先级的其他线程获得运行的机会因此,使用yield()的目的是让具有相同优先级的线程之间能够适当的轮换执行。但是,实际中无法保证`yield()达到让步的目的,因为,让步的线程可能被线程调度程序再次选中。

使用yield方法时要注意的几点:

  • yield是一个静态的方法。
  • 调用yield后,yield告诉当前线程把运行机会交给具有相同优先级的线程。
  • yield不能保证,当前线程迅速从运行状态切换到就绪状态。
  • yield只能是将当前线程从运行状态转换到就绪状态,而不能是等待或者阻塞状态。
public class YeildThread implements Runnable{
    public static void main(String[] args){
        Thread t1 = new Thread(new YeildThread());
        Thread t2 = new Thread(new YeildThread());
        t1.start();
        t2.start();
    }
    @Override
    public void run(){
        for(int i=0;i<30;i++){
            if("Thread-0".equals(Thread.currentThread().getName())){
                if(i==0){
                    //让步可能失败
                    Thread.yield();
                }
            }
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
    }
}

线程的联合

当前线程邀请调用方法的线程优先执行,在调用方法的线程执行结束之前,当前线程不能再次执行。线程A在运行期间,可以调用线程B的join()方法,让线程B和线程A联合。这样,线程A就必须等待线程B执行完毕后,才能继续执行(类似于方法调用,在A方法中调用B方法后,A方法会在B方法执行完后继续执行)。

join()方法的使用

class A implements Runnable{
@Override
public void run(){
    for(int i=0;i<10;i++){
        System.out.println(Thread.currentThread().getName()+" "+i);
        try{
            Thread.sleep(1000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        }
    }  
}
public class JoinThread{
    public static void main(String[] args){
        Thread t = new Thread(new A());
        
        //并行子线程A
        //t.start();
        
        //主线程main
        for(int i=0;i<10;i++){
            System.out.println(Thread.currentThread().getName()+" "+i);
            if(i==2){
                try{
                    //通过当前线程对象调用join方法进行联合
                    t.join();
                }catch(InterruptedException e){
                    e.printStackTrace();
                }
            }
            try{
                Thread.sleep(1000);
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}

若此时还有一个B线程,没有联合。当主线程和A线程联合后,主线程阻塞,A线程和B线程并行运行。

线程联合案例

案例:爸爸让儿子去买烟,儿子买回来,爸爸再去抽烟。

分析:共需要三个线程:主线程,爸爸抽烟的线程,儿子买烟的线程

public class JoinDemo{
    public static void main(String[] args){
        Thread t = new Thread(new FatherThread());
        //启动爸爸抽烟的线程
        t.start();
    }
}
//爸爸抽烟的线程
class FatherThread implements Runnable{
    @Override
    public void run(){
        System.out.println("爸爸想抽烟,发现烟抽完了。");
        System.out.println("爸爸让儿子去买一包烟。");
        //线程联合
        //启动儿子买烟的线程
        Thread t = new Thread(new SonThread());
        t.start();
        System.out.println("爸爸等待儿子买烟回来。");
        try{
            t.join();
        }catch (InterruptedException e){
            e.printStackTrace();
            System.out.println("爸爸出门找儿子");
            System.exit(0);
        }
        System.out.println("爸爸拿到烟,继续抽烟。");
    }
}

//儿子买烟的线程
class SonThread implements Runnable{
    @Override
    public void run(){
        System.out.println("给爸爸去买烟。");
        System.out.println("儿子买烟要花时间。");
            for(int i=0;i<10;i++){
                System.out.println("第"+i+"分钟");
                try{
                    Thread.sleep(2000);
                }catch (InterruptedException e){
                     e.printStackTrace();
                }
         }
    }
}

打印结果

爸爸想抽烟,发现烟抽完了。
爸爸让儿子去买一包烟。
爸爸等待儿子买烟回来。
给爸爸去买烟。
儿子买烟要花时间。
第0分钟
第1分钟
第2分钟
第3分钟
第4分钟
第5分钟
第6分钟
第7分钟
第8分钟
第9分钟
爸爸拿到烟,继续抽烟。

Thread类中的其他常用方法

getName()获取当前线程名称

  • 方式一(适用于通过继承Thread类实现的多线程)

this.getName()获取线程名称,该方法适用于继承Thread 实现多线程方式

class GetName1 extends Thread{
    @Override
    public void run(){
        //因为GetName1是直接继承了Thread,所以可以直接调用Thread中的公有方法getName()
        System.out.println(this.getName());
    }
}
public class GetNameThread{
    public static void main (String[] args){
        GetName1 getName1 = new GetName1();
        getName1.start();
    }
}
  • 方式二(适用于实现Runnable接口实现多线程)

Thread.currentThread().getName()获取线程名称,该方法适用于实现Runnable接口实现多线程方式。

class GetName2 implements Runnable{
    @Override
    public void run(){
        //线程类实现了Runnable接口,那么线程类无法直接拿到getName方法
        //Thread.currentThread()是一个静态方法,返回当前线程对象
        System.out.println(Thread.currentThread().getName());
    }
}
public class GetNameThread{
    public static void main (String[] args){
        Thread t = new Thread(new GetName2());
        t.start();
    }
}

setName()设置线程的名称

  • 方式一(适用于通过继承Thread类实现的多线程)

通过构造方法设置线程名称。

class SetName1 extends Thread{
    //Thread中含有多个有参和无参构造方法
    public SetName1(String name){
        //将设定的名称传递到Thread中
        super(name);
    }
    
    @Override
    public void run(){
        System.out.println(this.getName());
    }
}
public class SetNameThread{
    public static void main(String[] args){
        SetName1 setName1 = new SetName1("SetName1");
        setName1.setName("SetName1-test")
        setName1.start();
    }
}
  • 方式二(适用于实现Runnable接口实现多线程)

通过setName()方法设置线程名称。

class SetName2 implements Runnable{
    //Thread中含有多个有参和无参构造方法
    public SetName2(String name){
        //将设定的名称传递到Thread中
        super(name);
    }
    
    @Override
    public void run(){
        System.out.println(Thread.currentThread().getName());
    }
}
public class SetNameThread{
    public static void main(String[] args){
        Thread t = new Thread(new SetName2());
        thread.setName("SetName2");
        thread.start();
    }
}

isAlive()判断当前线程是否存活

isAlive()方法:判断当前的线程是否处于活动状态。
活动状态是指线程已经启动且尚未终止,线程处于正在运行或准备开始运行的状态,就认为线程是存活的。

class Alive implements Runnable{
    @Override
    public void run(){
        System.out.println("第二次:"+Thread.currentThread().isAlive());
        try{
            Thread.sleep(20000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }
}
public class AliveThread{
    public static void main(String[] args){
        Thread thread = new Thread(new Alive());
        System.out.println("第一次:"+thread.isAlive());
        thread.start();
        System.out.println("第三次:"+thread.isAlive());
        try{
            Thread.sleep(1000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        System.out.println("第四次:"+thread.isAlive());
    }
}

运行结果:

第一次:false
第三次:true
第二次:true
第四次:true

线程的优先级

每一个线程都是有优先级的,我们可以为每个线程定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。线程的优先级用数字表示,范围从1到10,一个线程的缺省优先级是5。

Java 的线程优先级调度会委托给操作系统去处理,所以与具体的操作系统优先级有关,如非特别需要,一般无需设置线程优先级。

注意:线程的优先级,不是说哪个线程优先执行,如果设置某个线程的优先级高。那就是有可能被执行的概率高。并不是优先执行。

线程优先级的使用

使用下列方法获得或设置线程对象的优先级

  • int getPriority()
  • void setPriority(int newPriority)

setPriority()设置线程优先级

class Priority implements Runnable {
    private int num = 0;
    private boolean flag = true;

    @Override
    public void run() {
        while (this.flag) {
            System.out.println(Thread.currentThread().getName() + " " + num++);
        }
    }
        public void stop () {
            this.flag = false;
        }
    }

    public  class PriorityThread{
        public static void main(String[] args)throws Exception{
            Priority p1 = new Priority();
            Priority p2 = new Priority();
            Thread t1 = new Thread(p1,"线程1");
            Thread t2 = new Thread(p2,"线程2");
            //Thread.MAX_PRIORITY 最高优先级10
            t1.setPriority(Thread.MAX_PRIORITY);
            //Thread.MIN_PRIORITY 最低优先级1
            t2.setPriority(Thread.MIN_PRIORITY);
            //启动线程
            t1.start();
            t2.start();
            Thread.sleep(1000);
            p1.stop();
            p2.stop();
        }
}

守护线程

在Java中有两类线程:

  • User Thread(用户线程):就是应用程序里的自定义线程。
  • Daemon Thread(守护线程):比如垃圾回收线程,就是最典型的守护线程。

守护线程(即Daemon Thread),是一个服务线程,准确地来说就是服务其他的线程,这是它的作用,而其他的线程只有一种,那就是用户线程。

守护线程特点:
守护线程会随着用户线程死亡而死亡。

守护线程和用户线程的区别

  • 用户线程,不随主线程的死亡而死亡。用户线程只有两种情况会死亡,一种是在run中异常终止,另一种是把run执行完毕,线程死亡。
  • 守护线程,随着用户线程的死亡而死亡,当用户线程死亡守护线程也会随之死亡。

setDaemon()添加守护线程

class Daemon implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i <20; i++) {
            System.out.println(Thread.currentThread().getName()+" "+i);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public class DaemonThread {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(new Daemon(),"Daemon");
        //将该线程设置为守护线程
        //此时是在主线程中设置,所以该守护线程为主线程服务
        t.setDaemon(true);
        t.start();
        Thread.sleep(1000);
        System.out.println("主线程结束");
    }
}

打印结果:

Daemon 0
主线程结束

守护线程只打印了一次i,就随主线程的死亡而死亡。

线程同步

什么是线程同步

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

线程冲突案例

银行取钱的基本流程基本上可以分为如下几个步骤。

  1. 用户输入账户、密码,系统判断用户的账户、密码是否匹配。
  2. 用户输入取款金额。
  3. 系统判断账户余额是否大于取款金额。
  4. 如果余额大于取款金额,则取钱成功;如果余额小于取款金额,则取钱失败。
class Account{
    private String accountNo;
    private double balance;

    public Account() {
    }

    public Account(String accountNo, double balance) {
        this.accountNo = accountNo;
        this.balance = balance;
    }

    public String getAccountNo() {
        return accountNo;
    }

    public void setAccountNo(String accountNo) {
        this.accountNo = accountNo;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }
}
//取钱线程
class DrawThread extends Thread{
    //账户对象
    private Account account;
    private double drawMoney;
    public DrawThread(String name,Account account,double drawMoney){
        //调用父类Thread构造方法,为线程命名
        super(name);

        this.account=account;
        this.drawMoney=drawMoney;
    }
    @Override
    public void run(){
        //判断当前账户余额
        if(this.account.getBalance()>=this.drawMoney){
            System.out.println(this.getName()+"取钱成功,"+this.drawMoney);
            //线程休眠
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            this.account.setBalance(this.account.getBalance()-this.drawMoney);
            System.out.println("余额:"+this.account.getBalance());
        }else{
            System.out.println(this.getName()+"余额不足");
        }
    }
}
public class DrawMoneyThread {
    public static void main(String[] args) {
        Account account=new Account("Jack",2000);
        new DrawThread("小明",account,1000).start();
        new DrawThread("小花",account,1500).start();
    }
}

打印结果:

小花取钱成功,2000.0
小明取钱成功,1000.0
余额:-1000.0
余额:0.0

线程冲突,前一个人没有将取的钱从余额扣除,后一个人就又取了钱,造成余额变成负数。

实现线程同步

synchronized关键字

由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突的问题。Java语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问造成的这种问题。这套机制就是synchronized关键字。
synchronized语法结构:

synchronized(锁对象){
    同步代码
}

synchronized 关键字使用时需要考虑的问题:

  • 需要对哪部分的代码在执行时具有线程互斥的能力(线程互斥:并行变串行)。
  • 需要对哪些线程中的代码具有互斥能力(通过synchronized锁对象来决定)。
synchronized方法

通过在方法声明中加入synchronized关键字来声明,语法如下:
public synchronized void accessVal(int newVal);
synchronized在方法声明时使用:放在范围操作符(public)之后,返回类型声明(void)之前。这时同一个对象下synchronized方法在多线程中执行时,该方法是同步的,即一次只能有一个线程进入该方法,其他线程要想在此时调用该方法,只能排队等候,当前线程(就是在synchronized方法内部的线程)执行完该方法后,别的线程才能进入。

synchronized

synchronized方法的缺陷:若将一个大的方法声明为synchronized 将会大大影响效
率。
Java为我们提供了更好的解决办法,那就是synchronized 块。块可以让我们精确
地控制到具体的“成员变量”,缩小同步的范围,提高效率。

对取钱案例采用线程同步方法完善

分析:

线程同步:取款线程

将账户作为线程锁对象,当两个人同时对一个账户操作时,会让先运行的线程进行操作,另一个线程进入阻塞,待第一个线程取完钱,对余额操作完后,第二个线程才进入取钱过程。

class Account{
    private String accountNo;
    private double balance;

    public Account() {
    }

    public Account(String accountNo, double balance) {
        this.accountNo = accountNo;
        this.balance = balance;
    }

    public String getAccountNo() {
        return accountNo;
    }

    public void setAccountNo(String accountNo) {
        this.accountNo = accountNo;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }
}
//取钱线程
class DrawThread extends Thread{
    //账户对象
    private Account account;
    private double drawMoney;
    public DrawThread(String name,Account account,double drawMoney){
        //调用父类Thread构造方法,为线程命名
        super(name);

        this.account=account;
        this.drawMoney=drawMoney;
    }
    @Override
    public void run(){
        //若synchronized()加到run方法上,run方法仍然是一个并发线程
        //run方法是一个特殊方法,是一个线程体方法
        synchronized(this.account){
        //判断当前账户余额
        if(this.account.getBalance()>=this.drawMoney){
            System.out.println(this.getName()+"取钱成功,"+this.drawMoney);
            //线程休眠
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            this.account.setBalance(this.account.getBalance()-this.drawMoney);
            System.out.println("余额:"+this.account.getBalance());
        }else{
            System.out.println(this.getName()+"余额不足");
            }
        }
    }
}
public class DrawMoneyThread {
    public static void main(String[] args) {
        Account account=new Account("Jack",2000);
        new DrawThread("小明",account,1000).start();
        new DrawThread("小花",account,1500).start();
    }
}

打印结果:

小明取钱成功,1000.0
余额:1000.0
小花余额不足

线程同步的使用

使用this作为线程对象锁

在不同线程中,相同对象中的synchronized会互斥。

语法结构:

//-----法一-----
synchronized(this){
    //同步代码
}

//-----法二-----
public synchronized void f(){
    //同步代码
}
this作为线程对象锁案例
class Programmer{
    private String name;
    public Programmer(String name){
        this.name=name;
    }
    //打开电脑方法
    //synchronized也可写到 public之前
    public void start(){
        synchronized (this) {
            try {
                System.out.println(this.name + "接通电源");
                Thread.sleep(500);
                System.out.println(this.name + "按开机键");
                Thread.sleep(500);
                System.out.println(this.name + "系统启动");
                Thread.sleep(500);
                System.out.println(this.name + "成功");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
    public void coding(){
        synchronized (this) {
            try {
                System.out.println(this.name + "打开IDEA");
                Thread.sleep(500);
                System.out.println(this.name + "IDEA打开成功");
                Thread.sleep(500);
                System.out.println(this.name + "打开项目");
                Thread.sleep(500);
                System.out.println(this.name + "工作");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
//打开电脑的线程
class Working extends Thread{
    private Programmer p;
    private String name;
    public Working(Programmer p){
        this.p=p;
    }
    @Override
    public void run() {
        this.p.start();
    }
}

//工作线程
class Working2 extends Thread{
    private Programmer p;
    public Working2(Programmer p){
        this.p=p;
    }
    @Override
    public void run() {
        this.p.coding();
    }
}
public class MainApp {
    public static void main(String[] args) {
        Programmer p = new Programmer("小明");
        Working w1=new Working(p);
        w1.start();
        Working2 w2=new Working2(p);
        w2.start();
    }
}

若不加synchronized锁,则会造成:

小明接通电源
小明打开IDEA
小明IDEA打开成功
小明按开机键
小明打开项目
小明系统启动
小明工作
小明成功

加线程锁后:

小明接通电源
小明按开机键
小明系统启动
小明成功
小明打开IDEA
小明IDEA打开成功
小明打开项目
小明工作

使用字符串作为线程对象锁

字符串本质上是一个对象,如果使用字符串作为线程对象锁,所有线程在执行synchronized时都会同步。

语法结构:

synchronized("字符串"){
    //同步代码
}
字符串作为线程对象锁案例
public class StudySyn {
    public static void main(String[] args) {
        Drink1 d1=new Drink1(new Programmer("张三"));
        Drink2 d2=new Drink2(new Programmer("李四"));
        d1.start();
        d2.start();
    }
}

class Programmer{
    private String name;
    public Programmer(String name){
        this.name=name;
    }

    public void start() {
        //利用字符串作为对象锁,可以填入任意字符串,甚至空串
        synchronized ("线程锁") {
            try {
                System.out.println(name + "打开饮水机");
                Thread.sleep(500);
                System.out.println(name + "放入杯子");
                Thread.sleep(500);
                System.out.println(name + "按下出水按钮");
                Thread.sleep(500);
                System.out.println(name + "拿走杯子");
                Thread.sleep(500);
                System.out.println(name + "关上饮水机");
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }
}

class Drink1 extends Thread{
    private Programmer p;
    public Drink1(Programmer p){
        this.p=p;
    }

    @Override
    public void run() {
        p.start();
    }
}

class Drink2 extends Thread{
    private Programmer p;
    public Drink2(Programmer p){
        this.p=p;
    }

    @Override
    public void run() {
        p.start();
    }
}

输出:

张三打开饮水机
张三放入杯子
张三按下出水按钮
张三拿走杯子
张三关上饮水机
李四打开饮水机
李四放入杯子
李四按下出水按钮
李四拿走杯子
李四关上饮水机

使用Class作为线程对象锁

Class是一个对象类型。

当一个类被实例化时A a = new A(),JVM首先会使用类加载器(ClassLoader)加载到内存中,然后根据类创建一个Class类型的对象,最后在完成初始化动作等,JVM会将Class对象缓存下来,当以后再用到时就不需要重新加载了。

在不同线程中,拥有相同Class对象中的synchronized会互斥。

语法结构:

synchronized(xx.class){
    //同步代码
}

或者:

synchronized public static void accessVal()

如果synchronized将静态方法作为对象锁,那么同步块在静态方法的{ }括号间;

如果synchronized将非静态方法作为对象锁,那么以this作为对象锁。

Class作为线程对象锁案例
public class ClassSyn {
    public static void main(String[] args) {
        new Drink01(new Programmer("小明")).start();
        new Drink02(new Programmer("小花")).start();
    }
}
class Programmer{
    private String name;
    public Programmer(String name){
        this.name=name;
    }
    public  void start(){
        synchronized (Programmer.class) {
            try {
                System.out.println(name + "打开饮水机");
                Thread.sleep(500);
                System.out.println(name + "放入杯子");
                Thread.sleep(500);
                System.out.println(name + "按下出水按钮");
                Thread.sleep(500);
                System.out.println(name + "拿走杯子");
                Thread.sleep(500);
                System.out.println(name + "关上饮水机");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

class Drink01 extends Thread{
    private Programmer p;
    public Drink01(Programmer p){
        this.p=p;
    }
    @Override
    public void run(){
        p.start();
    }
}

class Drink02 extends Thread{
    private Programmer p;
    public Drink02(Programmer p){
        this.p=p;
    }
    @Override
    public void run(){
        p.start();
    }
}

输出:

小明打开饮水机
小明放入杯子
小明按下出水按钮
小明拿走杯子
小明关上饮水机
小花打开饮水机
小花放入杯子
小花按下出水按钮
小花拿走杯子
小花关上饮水机

使用自定义对象作为线程对象锁

在不同线程中,拥有相同自定义对象中的synchronized会互斥。

语法结构:

synchronized(自定义对象){
    //同步代码
}
自定义对象作为线程对象锁案例

老师上课随机点名,点到的同学答“到”,然后老师选择下一个同学点名。

public class VoidSyn {
    public static void main(String[] args) {
        Teacher t=new Teacher("张三");
        new TeacherThread(t,"小明").start();
        new TeacherThread(t,"小花").start();
    }
}

class Teacher{
    private String name;
    public Teacher(String name){
        this.name=name;
    }
    public void start(String sName){
        //都是同一个Teacher运行方法,所以这里可以使用 this 实现线程同步
        //synchronized (this) {
            try {
                System.out.println(this.name + "叫" + sName);
                Thread.sleep(500);
                System.out.println(sName + "听到老师叫自己");
                Thread.sleep(500);
                System.out.println(sName + "答到");
                Thread.sleep(500);
                System.out.println(this.name + "挑选下一个人");
            } catch (Exception e) {
                e.printStackTrace();
            }
        //}
    }
}

class TeacherThread extends Thread{
    private Teacher t;
    private String sname;
    public TeacherThread(Teacher t,String sname){
        this.t=t;
        this.sname=sname;
    }


    @Override
    public void run() {
        //传入的Teacher对象都是同一个,所以可以使用 this.t 实现线程同步
        //synchronized (this.t) {
            t.start(sname);
        //}
    }
}

synchronized既可以放在线程对象中实现,也可以放到非线程对象中实现线程同步。

死锁及解决方案

死锁概念

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

死锁案例

例如现在有两个 喝水 线程:

线程1:小明想喝水(需要杯子和水桶)->拿到杯子->打开饮水机->接水

线程2:小花想喝水(需要杯子和水桶)->拿到杯子->打开饮水机->接水

但是杯子和水桶都只有一个,小明先去拿杯子花了0.5秒,回来发现小花已经拿到水桶了,此时小花没有水杯,有水桶;小明没有水桶,有水杯,都无法喝到水,而且两个人都在等对方喝完后把“资源”让出来,自己再喝。

public class ClockSyn {
    public static void main(String[] args) {
        Cub c=new Cub();
        Water w=new Water();
        new GetWaterThread(new GetWater(),"小明",c,w).start();
        new GetWaterThread2(new GetWater2(),"小花",c,w).start();
    }
}

//水杯类
class Cub{
    String name = "水杯";
}

//水桶类
class Water{
    String name = "水桶";
}

class GetWater{
    private Cub c;
    private Water w;
    public void start(String userName,Cub c,Water w){
        try{
            System.out.println(userName+"想喝水");
            //小明拿杯子花了0.5s
            synchronized (c) {
                System.out.println(userName+"拿到"+c.name);
                Thread.sleep(500);
                synchronized (w) {
                    System.out.println(userName + "拿着" + c.name + "打开" + w.name);
                    Thread.sleep(500);
                    System.out.println(userName + "用" + c.name + "接" + w.name + "的水");
                    Thread.sleep(500);
                    System.out.println(userName + "喝完" + c.name + "里的水");
                    Thread.sleep(500);
                    System.out.println(userName + "放回" + c.name);
                    Thread.sleep(500);
                    System.out.println(userName + "离开" + w.name);
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

class GetWater2{
    private Cub c;
    private Water w;
    public void start(String userName,Cub c,Water w){
        try{
            System.out.println(userName+"想喝水");
            //小花先拿到水桶
            synchronized (w) {
                System.out.println(userName+"拿到"+w.name);
                synchronized (c) {
                    System.out.println(userName + "拿着" + c.name + "打开" + w.name);
                    Thread.sleep(500);
                    System.out.println(userName + "用" + c.name + "接" + w.name + "的水");
                    Thread.sleep(500);
                    System.out.println(userName + "喝完" + c.name + "里的水");
                    Thread.sleep(500);
                    System.out.println(userName + "放回" + c.name);
                    Thread.sleep(500);
                    System.out.println(userName + "离开" + w.name);
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

//创建小明喝水线程
class GetWaterThread extends Thread{
    private GetWater gw;
    private String userName;
    private Cub c;
    private Water w;
    public GetWaterThread(GetWater gw,String userName,Cub c,Water w){
        this.gw=gw;
        this.userName=userName;
        this.c=c;
        this.w=w;
    }
    @Override
    public void run() {
        gw.start(userName,c,w);
    }
}

//创建小花喝水线程
class GetWaterThread2 extends Thread{
    private GetWater2 gw;
    private String userName;
    private Cub c;
    private Water w;
    public GetWaterThread2(GetWater2 gw,String userName,Cub c,Water w){
        this.gw=gw;
        this.userName=userName;
        this.c=c;
        this.w=w;
    }
    @Override
    public void run() {
        gw.start(userName,c,w);
    }
}

输出结果:

小明想喝水
小花想喝水
小明拿到水杯
小花拿到水桶

两个人都无法喝到水。

死锁解决方案

保持良好编码习惯,编写同一个代码块时,不要同时持有两个对象锁。

线程并发协作(生产者/消费者模式)

当有多个线程的情况下,我们经常需要多个线程并发、协作。

生产者:负责生产数据

消费者:负责“消费”数据

缓冲区:生产者将生产完的数据放入缓冲区,消费者再从缓冲区中拿取数据。

缓冲区可以减少生产者和消费者之间的耦合,提高效率。

生产者/ 消费者模式案例:

public class CsPdSyn {
    public static void main(String[] args) {
        BufferStack bs=new BufferStack();
        new Producer(bs).start();
        new Customer(bs).start();
    }
}

//生产者生产的“数据”
class ManTou{
    private int id;
    public ManTou(int id){
        this.id=id;
    }
}

//缓冲区
class BufferStack{
    //存放馒头的盒子
    private ManTou[] mt=new ManTou[10];
    //盒子内编号
    private int index;
    //放入馒头方法
    public synchronized void push(ManTou mt){
        while(this.index==this.mt.length){
            //wait() 会让当前线程进入阻塞状态,并且释放当前线程的对象锁
            //所以该方法必须在synchronized块中使用
            try {
                this.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        //避免两个线程都处于等待状态
        //notify()必须在synchronized块中调用
        //该方法会唤醒处于等待状态队列中的一个线程
        this.notify();
        this.mt[this.index]=mt;
        this.index++;
    }
    //取出馒头方法
    public synchronized void pop(){
        while(this.index==0){
            try {
                this.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        this.notify();
        this.mt[this.index]=null;
        this.index--;
    }
}

//生产者线程类
class Producer extends Thread{
    private BufferStack bs;
    public Producer(BufferStack bs){
        this.bs=bs;
    }
    @Override
    public void run() {
        for(int i=0;i<10;i++){
            System.out.println("生产者生产了馒头:"+i);
            ManTou mt=new ManTou(i);
            this.bs.push(mt);
        }
    }
}

//消费者线程类
class Customer extends Thread{
    private BufferStack bs;
    public Customer(BufferStack bs){
        this.bs=bs;
    }
    @Override
    public void run() {
        for(int i=0;i<10;i++){
            this.bs.pop();
            System.out.println("消费者消费了馒头:"+i);
        }
    }
}