多线程总结

*多线程*

1、线程概述

程序、进程、线程

程序(Program)是一个静态的概念,一般对应于操作系统中的一个可执行文件

进程(Process) 指的是执行中的程序,是一个动态的概念。其实进程就是一个在内存中独立运行的程序空间 ,即一个运行的程序。

每个进程由CPU、Data和Code三部分组成。

每个进程之间是完全独立、相互隔离的,都有自己的CPU的运行时间,进程之间的数据是不共享的。

一个程序可能有多个进程,一个进程却只对应一个程序。

一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。

线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。线程是程序最基本的运行单位,而进程不能运行,所以能运行的,是进程中的线程。

线程是进程内一个相对独立的、可调度的执行单元,又称为轻量级进程。线程必须拥有父进程。在java中,我们操作的都是对象,线程也不例外。是对象就有类,线程类就是Thread

main方法结束只代表主线程结束了,其他线程可能还在执行。

每一个进程至少包含一个线程;也可以包含多个并行进程,且这多个线程可以共享一个进程的相同内存单元的地址空间(物理空间),它们可以访问一个进程内的相同的变量和对象,它们从同一堆当中分配对象并执行通讯。所以它的数据交换和同步操作是非常容易的。

与进程不同的是同类的多个线程共享进程的方法区资源,但每个线程有自己的程序计数器虚拟机栈本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。

有些进程还不止同时干一件事,比如微信,它可以同时进行打字聊天,视频聊天,朋友圈等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。

线程与进程的区别

根本区别:线程是程序执行(处理器任务调度和执行)的最小单位,而进程是操作系统分配资源的最小单位。

资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的。

调度和切换:线程上下文切换比进程上下文切换要快得多。

影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行。

线程与方法的区别

方法的执行特点:串行。

线程的执行特点:并行。

并发、串行、并行

并发(concurrency) 是指在一段时间内同时做多个事情。当有多个线程在运行时,如果只有一个CPU,这种情况下计算机操作系统会采用并发技术实现并发运行,具体做法是采用“ 时间片轮询算法”,在一个时间段的线程代码运行时,其它线程处于就绪状。这种方式我们称之为并发。(Concurrent)。

一个CPU采用时间片管理方式,交替的处理多个任务。一般是是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)

串行(serial):一个CPU上,多个线程按顺序完成多个任务。

并行(parallelism) 指的是任务数小于等于cpu核数,即任务真的是一起执行的。

并发与并行的区别

并发是指两个程序或以上在同一时间段上发生。

并行是指两个程序或以上在同一时刻上发生(同时发生)。

并发是一个人做多件事,来回切换。

并行是多个人做多件事,互不干涉。

主线程与子线程

主线程(main thread)是指main方法对应的线程。 当Java程序启动时,main线程会立刻运行,它是程序开始时就执行的。

子线程 是指在主线程中创建并启动的线程。

Java应用程序会有一个main方法,是作为某个类的方法出现的。当程序启动时,该方法就会第一个自动的得到执行,并成为程序的主线程。也就是说,main方法是一个应用的入口,也代表了这个应用的主线程。JVM在执行main方法时,main方法会进入到栈内存,JVM会通过操作系统开辟一条main方法通向cpu的执行路径,cpu就可以通过这个路径来执行main方法,而这个路径有一个名字,叫main(主)线程。

主线程的特点:它是产生其他子线程的线程。

它不一定是最后完成执行的线程,子线程可能在它结束之后还在运行。

2、线程状态和生命周期

线程的执行流程

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

新生状态(New)

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

就绪状态(Runnable)

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

  1. 新建线程:调用start()方法,进入就绪状态

  2. 阻塞线程:阻塞解除,进入就绪状态;

  3. 运行线程:调用yield()方法,直接进入就绪状态;

  4. 运行线程:JVM将CPU资源从本线程切换到其他线程。

注意:

新建线程调用start()方法,不是进入运行状态,而是就绪状态。

运行状态(Running)

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

阻塞状态(Blocked)

阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪)。

有4种原因会导致阻塞:

  1. 执行sleep(int millsecond)方法,使当前线程休眠,进入阻塞状态。当指定的时间到了后,线程进入就绪状态。

  2. 执行wait()方法,使当前线程进入阻塞状态。当使用nofity()方法唤醒这个线程后,它进入就绪状态。

  3. 线程运行时,某个操作进入阻塞状态,比如执行IO流操作(read()/write()方法本身就是阻塞的方法)。只有当引起该操作阻塞的原因消失后,线程进入就绪状态。

  4. join()线程联合: 当某个线程等待另一个线程执行结束后,才能继续执行时,使用join()方法。

死亡状态(Terminated)

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

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

3、线程的创建

实现并启动线程的两种方法
  • 1、写一个类继承自 Thread 类,重写 run 方法。用 start 方法启动线程。

  • 2、写一个类实现 Runnable 接口,实现 run 方法。用 new Thread(Runnable target).start() 方法来启动。

多线程原理:相当于玩游戏机,只有一个游戏机(cpu),可是有很多人要玩,于是,start 是排队!等 CPU 选中你就是轮到你,你就 run(),当 CPU 的运行的时间片执行完,这个线程就继续排队,等待下一次的run()。

调用 start() 后,线程会被放到等待队列,等待 CPU 调度,并不一定要马上开始执行,只是将这个线程置于可动行状态。然后通过 JVM,线程 Thread 会调用 run() 方法,执行本线程的线程体。先调用 start 后调用 run,这么麻烦,为了不直接调用 run?就是为了实现多线程的优点,没这个 start 不行。

run() 与 start() 的区别

start:用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码。通过调用Thread类的 start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法,这里方法 run()称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。

run:run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。

总结:调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程里执行。

记住:多线程就是分时利用 CPU,宏观上让所有线程一起执行 ,也叫并发

Java 的线程是通过 java.lang.Thread 类来实现的。JVM 启动时会有一个由主方法所定义的线程。可以通过创建 Thread 的实例来创建新的线程。每个线程都是通过某个特定 Thread 对象所对应的run() 方法来完成其操作的,run() 方法称为线程体。通过调用 Thread 类的 start() 方法来启动一个线程。

Java 的线程是通过 java.lang.Thread 类来实现的。VM 启动时会有一个由主方法所定义的线程。可以通过创建 Thread 的实例来创建新的线程。每个线程都是通过某个特定 Thread 对象所对应的方法 run() 来完成其操作的,方法 run() 称为线程体。通过调用 Thread 类的 start() 方法来启动一个线程。

在 Java 当中,线程通常都有五种状态,创建、就绪、运行、阻塞和死亡。

  • 第一是创建状态。在生成线程对象,并没有调用该对象的 start 方法,这是线程处于创建状态。

  • 第二是就绪状态。当调用了线程对象的 start 方法之后,该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。

  • 第三是运行状态。线程调度程序将处于就绪状态的线程设置为当前线程,此时线程就进入了运行状态,开始运行 run 函数当中的代码。

  • 第四是阻塞状态。线程正在运行的时候,被暂停,通常是为了等待某个时间的发生(比如说某项资源就绪)之后再继续运行。sleep,suspend,wait 等方法都可以导致线程阻塞。

  • 第五是死亡状态。如果一个线程的 run 方法执行结束或者调用 stop 方法后,该线程就会死亡。对于已经死亡的线程,无法再使用 start 方法令其进入就绪。

继承Thread类实现多线程

通过继承Thread类实现多线程的步骤:

  1. 在Java中负责实现线程功能的类是java.lang.Thread 类。

    此种方式的缺点:如果我们的类已经继承了一个类(如小程序必须继承自 Applet 类),则无法再继承 Thread 类。

  2. 可以通过创建 Thread的实例来创建新的线程。

  3. 每个线程都是通过某个特定的Thread对象所对应的方法run( )来完成其操作的,方法run( )称为线程体。

  4. 通过调用Thread类的start()方法来启动一个线程。

实例1:通过继承Thread类实现多线程
package com.lg.tread;
​
/**
 * thread 线程
 * 通过继承Thread类实现多线程
 */
public class TestTread extends Thread {
    //线程方法,该方法不能直接调用,而是通过Thread类中的start()方法来启动。
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(this.getName()+":"+i);
        }
    }
    //main()主线程
    public static void main(String[] args) {
        //创建线程对象tread1
        TestTread tread1 = new TestTread();
        tread1.start();     //启动线程
        //创建线程对象tread2
        TestTread tread2 = new TestTread();
        tread2.start();
    }
}

以上代码,输出结果为:

Thread-1:0
Thread-0:0
Thread-1:1
Thread-0:1
Thread-1:2
Thread-0:2
Thread-1:3
Thread-0:3
Thread-0:4
Thread-1:4
Runnable接口实现多线程

在开发中,我们应用更多的是通过Runnable接口实现多线程。这种方式克服了继承Thread类的缺点,即在实现Runnable接口的同时还可以继承某个类。

从源码角度看,Thread类也是实现了Runnable接口。Runnable接口的源码如下:

public interface Runnable {
   void run();
}

两种方式比较看,实现Runnable接口的方式要通用一些。

实例2:通过Runnable接口实现多线程
package com.lg.tread;
​
import sun.awt.windows.ThemeReader;
​
/**
 * 通过Runnable接口实现多线程
 */
public class TestThread2 implements Runnable {
    //线程方法:如果此线程是使用单独的Runnable运行对象构造的,则调用该Runnable对象的run方法; 否则,此方法不执行任何操作并返回。
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            /**
             * currentThread():返回对当前正在执行的线程对象的引用。
             * getName():返回此线程的名称。
             */
            System.out.println(Thread.currentThread().getName()+":"+i);
        }
    }
    
    public static void main(String[] args) {
        //*包装成线程对象*
        Thread t1 = new Thread(new TestThread2());
        t1.start();
​
        Thread t2 = new Thread(new TestThread2());
        t2.start();
    }
}

以上代码,输出结果为:

Thread-0:0
Thread-1:0
Thread-1:1
Thread-1:2
Thread-1:3
Thread-1:4
Thread-0:1
Thread-0:2
Thread-0:3
Thread-0:4
获取线程名称
  • 方式一:this.getName()

  • 继承Thread类,使用Thread类中的getName()方法。

  • 方式二:Thread.currentThread().getName()

  • 实现Runnable接口。

实例3:获取当前线程名称
package com.lg.tread;
​
/**
 * GetNameThread 获取当前线程名称
 */
​
/**
 * 方式一:this.getName()
 *      继承Thread类,使用Thread类中的getName()方法。
 */
class GetName1 extends Thread {
    @Override
    public void run() {
        System.out.println(this.getName()+" Thread");
    }
}
​
/**
 * 方式二:Thread.currentThread().getName()
 *      实现Runnable接口。
 */
class GetName2 implements Runnable {
​
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+" Runnable");
    }
}
​
public class TestGetNameThread {
    public static void main(String[] args) {
        //创建GetName1线程对象
        GetName1 g = new GetName1();
        g.start();
        //包装GetName2成Thread对象
        Thread t = new Thread(new GetName2());
        t.start();
    }
}

以上代码,输出结果为:

Thread-0 Thread
Thread-1 Runnable
设置线程名称
  • 方式一:线程创建时通过继承Thread的方式来定义线程时:

  • 通过构造方法将线程的名字传递到线程对象当中,线程对象通过super()方法调用Thread的构造方法,将线程的名字传递给Thread。

  • 方式二:线程创建时通过实现Runnable接口的方式来定义线程时:

  • 通过对于线程包装以后的Thread对象来调用它的setName()方法类完成名称的修改。

实例4:设置线程名称1
package com.lg.tread;
​
/**
 * 设置线程名称
 */
​
/**
 * 方式一:线程创建时通过继承Thread的方式来定义线程时:
 * 通过构造方法将线程的名字传递到线程对象当中,线程对象通过super()方法调用Thread的构造方法,将线程的名字传递给Thread。
 */
class SetName1 extends Thread {
    //创建有参的构造方法
    public SetName1(String name) {
        super(name);    //super():调用超类(父类)的构造方法。
    }
​
    @Override
    public void run() {
        System.out.println(this.getName());
    }
}
​
/**
 * 方式二:线程创建时通过实现Runnable接口的方式来定义线程时:
 * 通过对于线程包装以后的Thread对象来调用它的setName()方法类完成名称的修改。
 */
class SetName2 implements Runnable {
​
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}
​
public class TestSetNameThread {
    public static void main(String[] args) {
        //创建线程对象SetName1
        SetName1 s = new SetName1("SetName1");
        s.start();
        //将SetName2包装成Thread对象
        Thread t = new Thread(new SetName2());
        t.setName("SetName2");  //修改线程名称
        t.start();
    }
}

以上代码,输出结果为:

SetName1
SetName2
判断线程是否存活

isAlive()方法: 判断当前的线程是否处于活动状态。

活动状态是指线程已经启动且尚未终止,线程处于正在运行或准备开始运行的状态,就认为线程是存活的。

实例5:测试线程是否活着
package com.lg.tread;
​
/**
 * AliveThread 测试线程是否活着
 * isAlive():用于测试线程是否活着。
 */
​
class Alive implements Runnable {
​
    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            System.out.println("线程名称:"+Thread.currentThread().getName() + " " + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
​
public class TestAliveThread {
    public static void main(String[] args) {
        //将Alive包装成Thread对象
        Thread t = new Thread(new Alive());
        t.setName("Alive");
        //新生状态:线程刚创建,未启动,并不是存活状态。
        System.out.println("线程刚创建时:"+t.getName()+" is "+t.isAlive()+" live");
        //就绪状态:线程刚启动,是存活状态。
        t.start();
        System.out.println("线程刚启动时:"+t.getName()+" is "+t.isAlive()+" live");
        //线程未执行完毕时,线程睡眠,线程存活。
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("线程睡眠时:"+t.getName()+" is "+t.isAlive()+" live");
    }
}

以上代码,输出结果为:

线程刚创建时:Alive is false live
线程刚启动时:Alive is true live
线程名称:Alive 1
线程名称:Alive 2
线程睡眠时:Alive is true live
线程名称:Alive 3
线程名称:Alive 4
线程名称:Alive 5

4、线程优先级

每一个线程都是有优先级的,我们可以为每个线程定义线程的优先级,但是线程优先级不能保证线程执行的顺序,而且非常依赖于平台。Java 的线程优先级调度会委托给操作系统去处理,所以与具体的操作系统优先级有关,如非特别需要,一般无需设置线程优先级。

线程的优先级是一个整数,其取值范围是 1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。数值越大,优先级越高,数值越小,优先级越低。

默认情况下,每一个线程都会分配一个优先级 NORM_PRIORITY(5)。一个线程的缺省优先级是5

注意:

线程优先级一定是在线程启动之前设置的。

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

实例6:获得或设置线程优先级
package com.lg.tread;
​
/**
 * PriorityThread 线程优先级
 *  getPriority():获取线程优先级,返回一个int。
 *  setPriority():设置线程优先级。
 * 注意:
 *  线程优先级一定是在线程启动之前设置的。
 */
class Priority implements Runnable {
    private int num = 0; //定义成员变量,累加
    private boolean falg = true; //定义生死牌
​
    @Override
    public void run() {
        while (this.falg) {
            System.out.println(Thread.currentThread().getName()+" "+num++);
        }
    }
​
    //终止线程方法
    public void stop() {
        this.falg = false;
    }
}
​
public class TestPriorityThread {
    public static void main(String[] args) {
        //实例化线程对象
        //Thread t1 = new Thread(new Priority(),"线程1");
        //Thread t2 = new Thread(new Priority(),"线程2");
        Priority p1 = new Priority();
        Priority p2 = new Priority();
        Thread t1 = new Thread(p1, "线程1");
        Thread t2 = new Thread(p2, "线程2");
        /**
         * getPriority():获取线程优先级,返回一个int。
         */
        System.out.println("t1的线程优先级:"+t1.getPriority());
        System.out.println("t2的线程优先级:"+t2.getPriority());
        /**
         * 改变线程优先级:
         *  setPriority():设置线程优先级。
         *  Thread.MAX_PRIORITY = 10
         *  Thread.MIN_PRIORITY = 1
         * 注意:
         *  线程优先级一定是在线程启动之前设置的。
         */
        t1.setPriority(Thread.MAX_PRIORITY);
        t2.setPriority(Thread.MIN_PRIORITY);
        System.out.println("改变后t1的线程优先级:"+t1.getPriority());
        System.out.println("改变后t2的线程优先级:"+t2.getPriority());
        //启动线程
        t1.start();
        t2.start();
        //使线程休眠1秒
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        //关闭线程
        p1.stop();
        p2.stop();
    }
}

以上代码,输出结果为:

。。。
线程1 184311
线程1 184312
线程2 68253

5、守护线程

Java中有两类线程:

  • User Thread(用户线程):就是应用程序里的自定义线程。

  • Daemon Thread(守护线程):比如垃圾回收线程,就是最典型的守护线程。

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

守护线程与用户线程的区别:

用户线程,不随着主线程的死亡而死亡。用户线程只有两种情况会死掉,1、在run中异常终止。2、正常把run执行完毕,线程死亡。

守护线程,随着用户线程的死亡而死亡,当用户线程死亡守护线程也会随之死亡。

守护线程(Daemon Thread),是一个服务线程,辅助线程,是为用户线程做服务的线程。

又称后台线程,精灵线程。

实例7:守护线程
package com.lg.tread;
​
/**
 * DaemonThread 守护线程
 * 创建守护线程时与创建用户线程没有任何区别。
 * setDaemon()  默认false
 *  true:守护线程;false:用户线程。
 */
class Daemon implements Runnable {
    @Override
    public void run() {
        //设置守护线程的生命周期
        for (int i = 1; i <= 20; i++) {
            //通过for循环来输出线程名称和迭代因子i。
            System.out.println(Thread.currentThread().getName()+" "+i);
            //使线程每隔一秒输出一次。
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
​
//线程类
class UserThread implements Runnable {
    @Override
    public void run() {
        //将Daemon包装成Thread线程对象,并起一个名字。
        Thread t = new Thread(new Daemon(),"Daemon");   //默认为用户线程
        /**
         * setDaemon()  默认false
         *  true:守护线程;false:用户线程。
         */
        t.setDaemon(true); //设置该线程为守护线程。
        t.start();
        //设置用户线程的生命周期
        for (int i = 1; i <= 5; i++) {
            //通过for循环来输出线程名称和迭代因子i。
            System.out.println(Thread.currentThread().getName()+" "+i);
            //使线程每隔一秒输出一次。
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
​
public class TestDaemonThread {
    public static void main(String[] args) {
        //将UserThread包装成Thread线程对象,并起一个名字。
        Thread t =new Thread(new UserThread(),"UserThream");
        t.start();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("主线程结束");
    }
}

以上代码,输出结果为:

UserThream 1
Daemon 1
UserThream 2
UserThream 3
UserThream 4
Daemon 2
主线程结束
UserThream 5

6、线程控制

终止线程

stop()/destroy():运行>>>死亡

stop() 方法容易导致死锁,通常不推荐使用。

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

实例8:终止线程
package com.lg.tread;
​
import java.io.IOException;
​
/**
 * StopThread 终止线程
 * stop() 方法容易导致死锁,通常不推荐使用。
 * stop():使线程死亡。
 */
public class TestStopThread implements Runnable {
    //定义生死牌,true:生,false:死
    private boolean flag = true;
    @Override
    public void run() {
        /**
         * Thread.currentThread():获取当前线程对象;static静态方法。
         * getName():获取当前线程名称。
         */
        System.out.println(Thread.currentThread().getName()+"线程开始");
        //定义一个变量
        int i = 0;
        while (this.flag) {
            System.out.println(Thread.currentThread().getName() + " " + i++);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println(Thread.currentThread().getName()+" 线程结束");
    }
    
    public void stop() {  //控制生死牌内容的方法
        this.flag = false;
    }
​
    public static void main(String[] args) throws IOException {
        System.out.println("主线程开始");
        //把TestStopThread的对象包装成线程对象Thread
        TestStopThread tst = new TestStopThread();
        Thread t = new Thread(tst);
        t.start();
        System.in.read();   
        tst.stop();     //终止线程
        System.out.println("主线程结束");
    }
}

以上代码,输出结果为:

主线程开始
Thread-0线程开始
Thread-0 0
Thread-0 1
Thread-0 2
Thread-0 3
Thread-0 4
Thread-0 5
a       //输入a使线程阻塞
主线程结束
Thread-0 线程结束
线程休眠

sleep():运行>>>阻塞

sleep()方法:可以让正在运行的线程进入阻塞状态,直到休眠时间满了,进入就绪状态。sleep方法的参数为休眠的毫秒数。

实例9:线程休眠
package com.lg.tread;
​
/**
 * SleepThread 线程休眠
 *             线程阻塞
 *   sleep():使线程休眠。
 */
public class TestSleepThread implements Runnable {
​
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+" 线程开始");
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName()+" "+i);
            //线程休眠1秒:运行>>>阻塞
            try {
                Thread.sleep(1000);   //使线程阻塞一秒,1s=1000ms
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
​
    public static void main(String[] args) {
        System.out.println("主线程开始");
        Thread t = new Thread(new TestSleepThread());
        t.start();
        System.out.println("主线程结束");
    }
}

以上代码,输出结果为:

主线程开始
主线程结束
Thread-0 线程开始   //每秒开始下一个线程
Thread-0 0
Thread-0 1
Thread-0 2
Thread-0 3
Thread-0 4
线程让步

yield():运行>>>就绪

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

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

  • yield是一个静态的方法。

  • 调用yield后,yield告诉当前线程把运行机会交给具有相同优先级的线程。

  • yield不能保证,当前线程迅速从运行状态切换到就绪状态。

  • yield只能是将当前线程从运行状态转换到就绪状态,而不能是等待或者阻塞状态。

实例10:线程让步
package com.lg.tread;
​
/**
 * YieldThread 线程让步
 * yield():暂停当前正在执行的线程对象,并执行其他线程。
 */
public class TestYieldThread implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            if ("Thread-0".equals(Thread.currentThread().getName())) {
                if (i == 0) {
                    System.out.println("线程t1让步");
                    Thread.yield();
                }
            }
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
    }
​
    public static void main(String[] args) {
        Thread t1 = new Thread(new TestYieldThread());
        Thread t2 = new Thread(new TestYieldThread());
        t1.start();
        t2.start();
    }
}

以上代码,输出结果为:

线程t1让步
Thread-1 0
Thread-1 1
Thread-1 2
Thread-1 3
Thread-1 4
Thread-0 0
Thread-0 1
Thread-0 2
Thread-0 3
Thread-0 4
线程联合

通过线程的联合,可以使线程由并行变为串行

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

join方法的使用

join()方法就是指调用该方法的线程在执行完run()方法后,再执行join方法后面的代码,即将两个线程合并,用于实现同步控制

t.join()方法只会使主线程(或者说调用t.join()的线程)进入等待池并等待t线程执行完毕后才会被唤醒,并不影响同一时刻处在运行状态的其他线程。

实例11:线程联合1

主线程联合A

package com.lg.tread;
​
/**
 * JoinThread 线程联合
 * 主线程联合A
 */
class A implements Runnable{    //子线程A
​
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName()+" "+i);
            try {
                Thread.sleep(1000);     //使线程A休眠1秒
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
​
class B implements Runnable{        //子线程B
​
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+" "+i);
            try {
                Thread.sleep(1000);     //使线程A休眠1秒
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
​
public class TestJoinThread {       //主线程
    public static void main(String[] args) {
        Thread t = new Thread(new A());
        Thread t1 = new Thread(new B());
        t.start();
        t1.start();
        //循环
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName()+" "+i);
            if (i == 2) {
                try {
                    t.join();       //主线程联合A
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            try {
                Thread.sleep(1000);     //使主线程休眠1秒
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

以上代码,输出结果为:

main 0
Thread-1 0
Thread-0 0
main 1
Thread-1 1
Thread-0 1
Thread-0 2
Thread-1 2
main 2
Thread-0 3
Thread-1 3
Thread-1 4
Thread-0 4
Thread-1 5
Thread-1 6
main 3
main 4
Thread-1 7
Thread-1 8
Thread-1 9
实例12:线程联合2

主线程联合A,A联合B

package com.lg.tread;
​
/**
 * JoinThread 线程联合
 * 主线程联合A,A联合B
 */
class A implements Runnable{    //子线程A
​
    //成员变量b
    private Thread b;
    //构造方法
    public A(Thread b) {
        this.b = b;
    }
​
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+" A "+i);
            if (i == 5) {
                try {
                    b.join();       //A联合B
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            try {
                Thread.sleep(1000);     //使线程A休眠1秒
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
​
class B implements Runnable{        //子线程B
​
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName()+" B "+i);
            try {
                Thread.sleep(1000);     //使线程A休眠1秒
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
​
public class TestJoinThread {       //主线程
    public static void main(String[] args) {
        //Thread t = new Thread(new A());
        //Thread t1 = new Thread(new B());
        Thread t1 = new Thread(new B());
        Thread t = new Thread(new A(t1));
        t.start();
        t1.start();
        //循环
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName()+" "+i);
            if (i == 2) {
                try {
                    t.join();   //主线程联合A
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            try {
                Thread.sleep(1000);     //使主线程休眠1秒
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

以上代码,输出结果为:

main 0
Thread-1 A 0
Thread-0 B 0
Thread-1 A 1
main 1
Thread-0 B 1
Thread-0 B 2
Thread-1 A 2
main 2
Thread-0 B 3
Thread-1 A 3
Thread-1 A 4
Thread-0 B 4
Thread-0 B 5
Thread-1 A 5
Thread-0 B 6
Thread-0 B 7
Thread-0 B 8
Thread-0 B 9
Thread-1 A 6
Thread-1 A 7
Thread-1 A 8
Thread-1 A 9
main 3
main 4
实例13:线程联合3

需求:实现爸爸让儿子买烟。

package com.lg.tread;
​
/**
 * FatherThread 劳资抽烟线程
 */
class FatherThread implements Runnable {
​
    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("劳资想抽烟,发现烟抽完了(꒪Д꒪)ノ");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("劳资让儿子去买一包红塔山 (*๓´╰╯`๓)");
        //叫儿子去买烟:启动儿子买烟线程
        Thread t = new Thread(new SonThread());
        t.start();      //启动儿子去买烟线程
        /**
         * 线程联合
         * 联合的线程需要等待被联合的线程执行完毕,联合的线程才能继续执行。
         * 劳资需要等待儿子将烟买回来,才能继续抽烟。
         */
        try {
            t.join();
        } catch (InterruptedException e) {
            //throw new RuntimeException(e);
            e.printStackTrace();
            //子线程出现异常
            System.out.println("儿子没买烟回来,拿钱去网吧玩了。。。");
            System.out.println("劳资出门找儿子去了(o´罒`o)哼!");
            System.exit(1); //结束线程,结束当前虚拟机。
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("劳资高兴地接过烟,并把零钱给了儿子(ノ゚∀゚)ノ ");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
​
/**
 * SonThread 儿子买烟线程
 */
class SonThread implements Runnable {
​
    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("儿子不情愿地出了门 -(ಥ_ಥ)");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("儿子买烟要10秒-=≡ヘ(*•̀ω•́)ノ");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        for (int i = 1; i <= 10; i++) {
            System.out.println("过了"+i+"秒─=≡Σ(((つ•̀ω•́)つ");
            try {
                Thread.sleep(1000);    //等待10秒
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println("儿子买烟回来了哈。。。┏|*´・Д・|┛");
    }
}
​
public class TestJoinDemo {
    public static void main(String[] args) {
        System.out.println("劳资让儿子的故事(*๓´╰╯`๓)");
        Thread t = new Thread(new FatherThread());
        t.start();  //启动劳资买烟的线程
    }
}

以上代码,输出结果为:

劳资让儿子的故事(*๓´╰╯`๓)
劳资想抽烟,发现烟抽完了(꒪Д꒪)ノ
劳资让儿子去买一包红塔山 (*๓´╰╯`๓)
儿子不情愿地出了门 -(ಥ_ಥ)
儿子买烟要10秒-=≡ヘ(*•̀ω•́)ノ
过了1秒─=≡Σ(((つ•̀ω•́)つ
过了2秒─=≡Σ(((つ•̀ω•́)つ
过了3秒─=≡Σ(((つ•̀ω•́)つ
过了4秒─=≡Σ(((つ•̀ω•́)つ
过了5秒─=≡Σ(((つ•̀ω•́)つ
过了6秒─=≡Σ(((つ•̀ω•́)つ
过了7秒─=≡Σ(((つ•̀ω•́)つ
过了8秒─=≡Σ(((つ•̀ω•́)つ
过了9秒─=≡Σ(((つ•̀ω•́)つ
过了10秒─=≡Σ(((つ•̀ω•́)つ
儿子买烟回来了哈。。。┏|*´・Д・|┛
劳资高兴地接过烟,并把零钱给了儿子(ノ゚∀゚)ノ

7、线程同步

线程冲突现象

现实生活中,我们会遇到“同一个资源,多个人都想使用”的问题。 比如:教室里,只有一台电脑,多个人都想使用。天然的解决办法就是,在电脑旁边,大家排队。前一人使用完后,后一人再使用。

线程同步:并行化>>>串行化

线程同步是为了解决线程冲突问题。

实例14:银行取钱时线程冲突

我们以银行取款经典案例来演示线程冲突现象。

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

(1)用户输入账户、密码,系统判断用户的账户、密码是否匹配。

(2)用户输入取款金额

(3)系统判断账户余额是否大于或等于取款金额

(4)如果余额大于或等于取款金额,则取钱成功;如果余额小于取款金额,则取钱失败。

package com.lg.sync;
​
/**
 * DrawMoneyThread 银行取钱线程冲突案例
 */
​
/**
 * Account 账户类
 */
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;
    }
}
​
/**
 * DrawThread 取款线程
 */
class DrawThread implements Runnable {
    private Account account;    //账户对象
    private double drawMoney;   //取款金额
​
    public DrawThread() {       //无参构造方法
    }
​
    public DrawThread(Account account, double drawMoney) {  //有参构造方法
        this.account = account;
        this.drawMoney = drawMoney;
    }
​
    @Override
    public void run() {     //取款线程体
        //判断当前账户余额是否大于或等于取款金额
        if (this.account.getBalance() >= this.drawMoney) {
            System.out.println(Thread.currentThread().getName()+" 取钱成功!拿出钞票"+this.drawMoney);
            try {
                Thread.sleep(1000); //使线程休眠1秒
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            //更新账户余额
            this.account.setBalance(this.account.getBalance() - this.drawMoney);
            System.out.println("\t 余额为:"+this.account.getBalance());
​
        } else {
            System.out.println(Thread.currentThread().getName()+" 取钱失败,余额不足!");
        }
    }
}
​
public class TestDrawMoneyThread {
    public static void main(String[] args) {
        //实例化银行账户
        Account a = new Account("1001",1000);
        //创建两个取款线程
        new Thread(new DrawThread(a, 600), "老公").start();
        new Thread(new DrawThread(a, 600), "老婆").start();
        /*Thread t1 = new Thread(new DrawThread(a, 600), "老公");
        Thread t2 = new Thread(new DrawThread(a, 600), "老婆");
        t1.start();
        t2.start();*/
    }
}

以上代码,输出结果为:

老公 取钱成功!拿出钞票600.0
老婆 取钱成功!拿出钞票600.0
     余额为:400.0
     余额为:400.0
线程同步的概念

对于多线程程序来说,同步是指在一定的时间内只允许某一个线程来访问某个资源。而在此时间内,不允许其他的线程访问该资源。可以通过互斥锁(Mutex)、条件变量(condition variable)、读写锁(reader-writer lock)、信号量(semaphore)来同步资源。

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

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

同步与互斥的区别

互斥:是指散布在不同进程之间的若干程序片段,当某个进程执行其中的一个程序片段时,其他进程就不能运行它们之中的任一程序片段,只能等到该进程运行完之后才可以继续运行。

同步:是指散布在不同进程之间的若干程序片段,它们的运行必须严格按照一定的先后次序来运行,这种次序依赖于要完成的任务。比如数据的收发,必须发送方发送了接收方才能收。

互斥是两个进程或线程之间不可以同时运行,它们会互相排斥,必须等待其中的一个运行完,另一个才可以运行。而同步也是不可以同时运行,并且还需要按照某种顺序来运行。

区别

互斥是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法控制对资源的访问顺序

同步是指在互斥的基础上实现对资源的有序访问

实现互斥的方法
  • 加锁

  • 信号量(P、V操作)

  • 硬件支持:

    • 中断禁用:单处理机中,在进程执行临界区代码时不可以被中断。

    • 专用机器指令:保证动作的原子性。

实现同步的方法
  • 信号量(P、V操作)

  • 消息传递:send()、recv()

  • 条件变量

*synchronized关键字*

synchronized关键字,用它来修饰需要同步的方法和需要同步代码块,默认是当前对象作为锁的对象。在用类修饰synchronized时(或者修饰静态方法),默认是当前类的Class对象作为锁的对象,故存在着方法锁、对象锁、类锁这样的概念。

synchronized是用于实现线程同步的。

synchronized的锁只能是对象类型

synchronized关键字可以修饰方法,可以修饰代码块,但不能修饰构造器成员变量等。

synchronized语法结构
synchronized(对象锁){  //同步代码  }
synchronized使用时需要考虑的问题:
  • 需要对那部分的代码在执行时具有线程互斥的能力(线程互斥并行变串行)。

  • 需要对哪些线程中的代码具有互斥能力(通过synchronized锁对象来决定)。

synchronized的两种用法

synchronized 方法和 synchronized 块。

(1)synchronized 方法

通过在方法声明中加入 synchronized关键字来声明,语法结构:

synchronized(this){ //同步代码 }

public synchronized void accessVal(int newVal){//同步代码}

注意:synchronized加到非静态方法上,它的对象锁就是this。

synchronized 在方法声明时使用:放在访问控制符(public)之前或之后。这时同一个对象下synchronized方法在多线程中执行时,该方法是同步的,即一次只能有一个线程进入该方法,其他线程要想在此时调用该方法,只能排队等候,当前线程(就是在synchronized方法内部的线程)执行完该方法后,别的线程才能进入。

使用this作为线程对象锁时,必须使用相同的对象,synchronized同步锁才会互斥。

(2)synchronized块

synchronized 方法的缺陷:若将一个大的方法声明为synchronized 将会大大影响效率。

Java 为我们提供了更好的解决办法,那就是 synchronized 块。 块可以让我们精确地控制到具体的“成员变量”,缩小同步的范围,提高效率。

线程同步的实现
实例15: 使用同步锁解决银行取钱线程冲突
package com.lg.sync;
​
/**
 * DrawMoneyThread 使用同步锁解决银行取钱线程冲突案例
 * Balance:余额 n. 平衡;均衡;天平; v. 抵消;相抵;使(在某物上)保持平衡;立稳; 网络释义: 差额;结余;平衡性;
 * synchronized(锁对象) {同步代码}   实现线程同步。
 */
​
/**
 * Account 账户类
 */
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;
    }
}
​
/**
 * DrawThread 取款线程
 */
class DrawThread implements Runnable {
    private Account account;    //账户对象
    private double drawMoney;   //取款金额
​
    public DrawThread() {       //无参构造方法
    }
​
    public DrawThread(Account account, double drawMoney) {  //有参构造方法
        this.account = account;
        this.drawMoney = drawMoney;
    }
​
    @Override
    public void run() {     //取款线程体.  在run方法上添加synchronized没有线程同步的效果,因为run方法是一个线程运行时的一个方法。
        synchronized (this.account) {   //synchronized():用于实现线程同步,this.account:锁对象。
            //判断当前账户余额是否大于或等于取款金额
            if (this.account.getBalance() >= this.drawMoney) {
                System.out.println(Thread.currentThread().getName()+" 取钱成功!拿走钞票"+this.drawMoney+"元");
                try {
                    Thread.sleep(1000); //使线程休眠1秒
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                //更新账户余额
                this.account.setBalance(this.account.getBalance() - this.drawMoney);
                ///System.out.println("\t 账户余额为:"+this.account.getBalance());
                System.out.println("\t"+this.account.getAccountNO()+"账户余额为:"+this.account.getBalance()+"元");
​
            } else {
                System.out.println(Thread.currentThread().getName()+" 取钱失败,余额不足!");
            }
        }
    }
}
​
public class TestDrawMoneyThread {
    public static void main(String[] args) {
        //实例化银行账户:a为老公的账户1001,b为老婆的账户1002。
        Account a = new Account("1001",1000);   //a:锁对象,即account
        Account b = new Account("1002",1000);   //a:锁对象,即account
​
        //创建两个取款线程:老公和老婆同时从老公的账户取钱。
        new Thread(new DrawThread(a, 400), "老公").start();
        new Thread(new DrawThread(a, 600), "老婆").start();
        new Thread(new DrawThread(b, 600), "老婆").start();
        /*Thread t1 = new Thread(new DrawThread(a, 600), "老公");
        Thread t2 = new Thread(new DrawThread(a, 600), "老婆");
        t1.start();
        t2.start();*/
    }
}

以上代码,输出结果为:

老公 取钱成功!拿走钞票400.0元
老婆 取钱成功!拿走钞票600.0元
    1001账户余额为:600.0元
    1002账户余额为:400.0元
老婆 取钱成功!拿走钞票600.0元
    1001账户余额为:0.0元
实例16:使用this作为线程对象锁*

使用this作为线程对象锁时,必须使用相同的对象,synchronized同步锁才会互斥。

语法结构:

synchronized(this){ //同步代码 }

public synchronized void accessVal(int newVal){//同步代码}
package com.lg.sync;
​
/**
 * ThisThread 使用this作为线程对象锁
 * 程序员工作案例
 * 使用this作为线程对象锁时,必须使用相同的对象,synchronized同步锁才会互斥。
 * 互斥无法控制对资源的访问顺序。
 */
​
/**
 * 定义程序员
 */
class Programmer {
    private String name;    //定义属性
​
    public Programmer(String name) {
        this.name = name;
    }
​
    //定义行为
​
    /**
     * 打开电脑
     */
    public void computer() {
        synchronized (this) {
            try {
                System.out.println(this.name+" 接通电源");
                Thread.sleep(1000);      //Thread.sleep(); 使线程休眠,可以出现在任何类当中。
                System.out.println(this.name+" 按开机按钮");
                Thread.sleep(1000);
                System.out.println(this.name+" 系统启动中。。。");
                Thread.sleep(1000);
                System.out.println(this.name+" 系统启动成功!");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
​
    /**
     * 敲代码
     */
    public void coding() {
        synchronized (this) {
            try {
                System.out.println(this.name+" 双击Idea");
                Thread.sleep(1000);
                System.out.println(this.name+" Idea启动完毕!");
                Thread.sleep(1000);
                System.out.println(this.name+" 开开心心的写代码^V^");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
​
/**
 * 打开电脑的工作线程
 */
class Working extends Thread {
    private Programmer p;   //创建程序员对象
​
    public Working(Programmer p) {
        this.p = p;
    }
​
    @Override
    public void run() {
        this.p.computer();      //打开电脑线程
    }
}
​
/**
 * 敲代码的工作线程
 */
class Working2 extends Thread {
    private Programmer p;
​
    public Working2(Programmer p) {
        this.p = p;
    }
​
    @Override
    public void run() {
        this.p.coding();    //敲代码的工作线程
    }
}
​
public class TestThisThread {
    public static void main(String[] args) {
        //实例化一个程序员
        Programmer p = new Programmer("Li");
        new Working(p).start();
        new Working2(p).start();
    }
}

以上代码,输出结果为:

Li 接通电源
Li 按开机按钮
Li 系统启动中。。。
Li 系统启动成功!
Li 双击Idea
Li Idea启动完毕!
Li 开开心心的写代码^V^
实例17:使用字符串作为线程对象锁

当有两个不同的对象时,使用字符串作为线程对象锁,synchronized同步锁才会互斥(字符串的内容随便)。

语法结构:

synchronized(“字符串”){ //同步代码 }
package com.lg.sync;
​
/**
 * StringThread 使用字符串作为线程对象锁
 * 程序员工作时上厕所案例
 * 当有两个不同的对象时,使用字符串作为线程对象锁,synchronized同步锁才会互斥(字符串的内容随便)。
 */
​
/**
 * 定义程序员
 */
class Programmer2 {
    //定义属性
    private String name;
​
    public Programmer2(String name) {
        this.name = name;
    }
​
    //定义行为
​
    /**
     * 打开电脑的行为
     */
    public void computer2() {
        synchronized ("p") {    //synchronized ("p"):当有两个不同的对象时,使用字符串作为线程对象锁,字符串的内容随便。
            try {
                System.out.println(this.name+" 接通电源");
                Thread.sleep(1000);      //Thread.sleep(); 使线程休眠,可以出现在任何类当中。
                System.out.println(this.name+" 按开机按钮");
                Thread.sleep(1000);
                System.out.println(this.name+" 系统启动中。。。");
                Thread.sleep(1000);
                System.out.println(this.name+" 系统启动成功!");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
​
    /**
     * 敲代码的行为
     */
    public void coding2() {
        synchronized ("p") {    //synchronized ("p"):当有两个不同的对象时,使用字符串作为线程对象锁,字符串的内容随便。
            try {
                System.out.println(this.name+" 双击Idea");
                Thread.sleep(1000);
                System.out.println(this.name+" Idea启动完毕!");
                Thread.sleep(1000);
                System.out.println(this.name+" 开开心心的写代码^V^");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
​
    /**
     * 上厕所的行为
     */
    public void wc() {
        synchronized ("p") {    //synchronized ("p"):当有两个不同的对象时,使用字符串作为线程对象锁,不同程序员上厕所的线程才会互斥。
            try {
                System.out.println(this.name+" 突然肚子疼");
                Thread.sleep(1000);
                System.out.println(this.name+" 狂奔去厕所");
                Thread.sleep(1000);
                System.out.println(this.name+" 蹲在茅坑上o(´^`)o");
                Thread.sleep(1000);
                System.out.println(this.name+" 扑通扑通┗( ▔, ▔ )┛");
                Thread.sleep(1000);
                System.out.println(this.name+" 冲水");
                Thread.sleep(1000);
                System.out.println(this.name+" 离开厕所");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
​
/**
 * 打开电脑的工作线程3
 */
class Working3 extends Thread {
    private Programmer2 p;   //创建程序员对象,成员变量
​
    public Working3(Programmer2 p) {
        this.p = p;
    }
​
    @Override
    public void run() {
        this.p.computer2();      //打开电脑线程
    }
}
​
/**
 * 敲代码的工作线程4
 */
class Working4 extends Thread {
    private Programmer2 p;   //创建程序员对象
​
    public Working4(Programmer2 p) {
        this.p = p;
    }
​
    @Override
    public void run() {
        this.p.coding2();    //敲代码的工作线程
    }
}
​
/**
 * 上厕所的线程5
 */
class WC extends Thread {
    private Programmer2 p;   //创建程序员对象
​
    public WC(Programmer2 p) {
        this.p = p;
    }
​
    @Override
    public void run() {
        this.p.wc();
    }
}
​
​
public class TestStringThread {
    public static void main(String[] args) {
        //实例化两个程序员
        Programmer2 p = new Programmer2("Li gen");
        Programmer2 p2 = new Programmer2("Zhang san");
        new Working3(p).start();
        new Working4(p).start();
        new WC(p).start();
        new WC(p2).start();
    }
}

以上代码,输出结果为:

Li gen 接通电源
Li gen 按开机按钮
Li gen 系统启动中。。。
Li gen 系统启动成功!
Li gen 双击Idea
Li gen Idea启动完毕!
Li gen 开开心心的写代码^V^
Zhang san 突然肚子疼
Zhang san 狂奔去厕所
Zhang san 蹲在茅坑上o(´^`)o
Zhang san 扑通扑通┗( ▔, ▔ )┛
Zhang san 冲水
Zhang san 离开厕所
Li gen 突然肚子疼
Li gen 狂奔去厕所
Li gen 蹲在茅坑上o(´^`)o
Li gen 扑通扑通┗( ▔, ▔ )┛
Li gen 冲水
Li gen 离开厕所
实例18:使用Class作为线程对象锁

使用Class作为线程对象锁时,必须使用相同的Class对象,synchronized同步锁才会互斥。

synchronized ():当有两个不同的对象时,使用Class对象作为线程对象锁,同一部门的不同员工领奖金会互斥,不同部门的不同员工会并行。

Class是指我们所创建的类的对象类型。

语法结构:

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

synchronized public static void accessVal()

在一个静态方法加上synchronized,默认使用当前这个方法所在的类的Class对象作为线程的对象锁(类锁)。

因为静态方法中没有this。

package com.lg.sync;
​
/**
 * StringThread 使用Class作为线程对象锁
 * 不同部门的不同员工领奖金案例
 * synchronized ():当有两个不同的对象时,使用Class对象作为线程对象锁,同一部门的不同员工领奖金会互斥,不同部门的不同员工会并行。
 * 互斥无法控制对资源的访问顺序。
 */
​
/**
 * 定义程序员类
 */
class Employee {
    //定义属性
    private String name;
​
    public Employee(String name) {
        this.name = name;
    }
​
    //定义行为 方法
    /**
     * 程序员领取奖金的行为
     */
    public void money() {
        synchronized (Employee.class) {    //synchronized ():当有两个不同的对象时,使用Class对象作为线程对象锁,同一部门的不同员工领奖金会互斥。
            try {
                System.out.println(this.name + " 找领导领奖金");
                Thread.sleep(1000);
                System.out.println(this.name + " 领了奖金");
                Thread.sleep(1000);
                System.out.println(this.name + " 感谢公司,感谢老板,感谢上帝!");
                Thread.sleep(1000);
                System.out.println(this.name + " 开开心心的拿钱走人^V^");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
​
/**
 * 定义销售部员工类
 */
class Sale {
    private String name;
​
    public Sale(String name) {
        this.name = name;
    }
​
    /**
     * 销售部员工领取奖金的行为
     */
    public void money() {
        synchronized (Sale.class) {    //synchronized ():当有两个不同的对象时,使用Class对象作为线程对象锁,同一部门的不同员工领奖金会互斥。
            try {
                System.out.println(this.name + " 找领导领奖金");
                Thread.sleep(1000);
                System.out.println(this.name + " 领了奖金");
                Thread.sleep(1000);
                System.out.println(this.name + " 感谢公司,感谢老板,感谢上帝!");
                Thread.sleep(1000);
                System.out.println(this.name + " 开开心心的拿钱走人^V^");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
​
}
​
/**
 * 程序员领奖金的线程
  */
 class PrizeMoney extends Thread {
    private Employee e;
​
    public PrizeMoney(Employee e) {
        this.e = e;
    }
​
    @Override
    public void run() {
        this.e.money();
    }
}
​
/**
 * 销售部员工领奖金的线程
 */
class SaleMoney extends Thread {
    private Sale s;
​
    public SaleMoney(Sale s) {
        this.s = s;
    }
​
    @Override
    public void run() {
        this.s.money();
    }
}
​
public  class ThisClassThread {
    public static void main(String[] args) {
        //实例化两个程序员
        Employee e = new Employee("Li gen");
        Employee e2 = new Employee("Zhang san");
        new PrizeMoney(e).start();
        new PrizeMoney(e2).start();
        //创建两个销售部员工
        Sale s = new Sale("Li si");
        Sale s2 = new Sale("Wang wu");
        new SaleMoney(s).start();
        new SaleMoney(s2).start();
    }
}

以上代码,输出结果为:

Li gen 找领导领奖金
Li si 找领导领奖金
Li si 领了奖金
Li gen 领了奖金
Li si 感谢公司,感谢老板,感谢上帝!
Li gen 感谢公司,感谢老板,感谢上帝!
Li gen 开开心心的拿钱走人^V^
Li si 开开心心的拿钱走人^V^
Zhang san 找领导领奖金
Wang wu 找领导领奖金
Zhang san 领了奖金
Wang wu 领了奖金
Zhang san 感谢公司,感谢老板,感谢上帝!
Wang wu 感谢公司,感谢老板,感谢上帝!
Wang wu 开开心心的拿钱走人^V^
Zhang san 开开心心的拿钱走人^V^
实例19:使用自定义对象作为线程对象锁

当有两个不同的对象调用同一个对象时,使用自定义对象作为线程对象锁,synchronized同步锁会互斥。

语法结构:

synchronized(自定义对象){ //同步代码 }
package com.lg.sync;
​
/**
 * CustomThread 使用自定义对象作为线程对象锁
 * synchronized (this.manager):当有两个不同的对象调用同一个对象时,使用自定义对象作为线程对象锁,synchronized同步锁会互斥。
 *      this:表示CheersThread线程对象。this.manager:表示经理对象。
 * Custom:自定义
 */
​
/**
 * 定义经理类
 */
class Manager {
    private String name;    //属性
​
    public Manager(String name) {   //构造方法
        this.name = name;
    }
​
    public String getName() {   //获取经理的name。
        return this.name;
    }
​
    /**
     * 敬酒的行为-方法
     */
    public void cheers(String mName,String eName) {     //mName:经理名字,eName:员工名字。
        try {
            System.out.println(mName + " 来到 " + eName + " 的面前");
            Thread.sleep(1000);
            System.out.println(eName + " 也拿起酒杯");
            Thread.sleep(1000);
            System.out.println(mName + " 和 " + eName + " 一起Cheers!");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
​
/**
 * 敬酒的线程
 */
class CheersThread extends Thread {
    //创建经理对象以及名字
    private Manager manager;
    private String eName;
​
    public CheersThread(Manager manager, String eName) {    //构造方法
        this.manager = manager;
        this.eName = eName;
    }
​
    @Override
    public void run() {     //运行
        synchronized (this.manager) {    //this:表示CheersThread线程对象。this.manager:表示经理对象。
            this.manager.cheers(this.manager.getName(),this.eName);
        }
    }
}
​
public class TestCustomThread {
    public static void main(String[] args) {
        //创建经理对象
        Manager manager = new Manager("Mr.Li");
        new CheersThread(manager, "Zhang san").start(); //敬酒:Mr.Li为Zhang san敬酒。
        new CheersThread(manager, "Li si").start();
    }
}

以上代码,输出结果为:

Mr.Li 来到 Zhang san 的面前
Zhang san 也拿起酒杯
Mr.Li 和 Zhang san 一起Cheers!
Mr.Li 来到 Li si 的面前
Li si 也拿起酒杯
Mr.Li 和 Li si 一起Cheers
线程死锁

“死锁”指的是:两个或以上线程各自占有一些共享资源,并且互相等待其他线程占有的资源才能进行,而导致两个或者多个线程都在等待对方释放资源,都停止执行的情形。

产生死锁的四个必要条件
  • 互斥条件:线程(进程)对于所分配到的资源具有排它性,即一个资源只能被一个线程(进程)占用,直到被该线程(进程)释放。

  • 请求与保持条件:一个线程(进程)因请求被占用资源而发生阻塞时,对已获得的资源保持不放,占有且等待

  • 不剥夺条件:线程(进程)已获得的资源在末使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。

  • 循环等待条件:当发生死锁时,所等待的线程(进程)必定会形成一个环路(类似于死循环),造成永久阻塞。

如何避免线程死锁

我们只要破坏产生死锁的四个条件中的其中一个就可以了。

  • 破坏互斥条件

    这个条件我们没有办法破坏,因为我们用锁本来就是想让他们互斥的(临界资源需要互斥访问)。

  • 破坏请求与保持条件

    一次性申请所有的资源。

  • 破坏不剥夺条件

    占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。

  • 破坏循环等待条件

    按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

具体的方法

  • 加锁顺序

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

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

  • 加锁时限

    在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求

    若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回滚并释放所有已经获得的锁,然后等待一段随机的时间再重试。

    这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行(加锁超时后可以先继续运行干点其它事情,再回头来重复之前加锁的逻辑)。

  • 死锁检测

    死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。

    每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。

    当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。

    那么当检测出死锁时,这些线程该做些什么呢?

    一个可行的做法是释放所有锁,回滚,并且等待一段随机的时间后重试。这个和简单的加锁超时类似,不一样的是只有死锁已经发生了才回滚,而不会是因为加锁的请求超时了。虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁。

    一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回滚,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级。

实例20:线程死锁案例
package com.lg.sync;
​
/**
 * DeadlockThread 死锁线程
 */
​
/**
 * 口红类
 */
class Lipstick {
​
}
​
/**
 * 镜子类
 */
class Mirror {
​
}
​
/**
 * 化妆线程类
 */
class Makeup extends Thread {
    //定义成员变量
    private  int flag;  //flag = 0:拿着口红,flag != 0:拿着镜子。
    private String girlName;    //girlName:女孩的名字
    //static:唯一的,静态的,用来定义唯一的口红和镜子对象。
    static Lipstick lipstick = new Lipstick();
    static Mirror mirror = new Mirror();
​
    public Makeup(int flag, String girlName) {  //构造方法
        this.flag = flag;
        this.girlName = girlName;
    }
​
    @Override
    public void run() {     //运行
        this.doMakeup();    //调用化妆方法
    }
​
    /**
     * 定义化妆的方法
     */
    public void doMakeup() {
        if (this.flag == 0) {
            synchronized (lipstick) {   //synchronized (lipstick):设置口红为对象锁,当我拿着口红,别人就不能拿口红了。
                System.out.println(this.girlName + " 拿着口红");
                try {
                    Thread.sleep(1000); //使线程休眠一秒
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (mirror) {
                    System.out.println(this.girlName + " 拿着镜子");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        } else {
            synchronized (mirror) {
                System.out.println(this.girlName + " 拿着镜子");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (lipstick) {
                    System.out.println(this.girlName + " 拿着口红");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
    }
}
​
public class TestDeadlockThread {
    public static void main(String[] args) {
        new Makeup(0,"小红").start();
        new Makeup(1,"小美").start();
    }
}

以上代码,输出结果为:

小红 拿着口红
小美 拿着镜子

某一个同步块需要同时拥有“两个以上对象的锁”时,就可能会发生“死锁”的问题。比如,“化妆线程”需要同时拥有“镜子对象”、“口红对象”才能运行同步块。那么,实际运行时,“小美的化妆线程”拥有了“镜子对象”,“大红的化妆线程”拥有了“口红对象”,都在互相等待对方释放资源,才能化妆。这样,两个线程就形成了互相等待,无法继续运行的“死锁状态”。

程序阻塞,出现线程死锁。死锁是由于 “同步块需要同时持有多个对象锁造成”的。

要解决这个问题,思路很简单,就是:同一个代码块,不要同时持有两个对象锁。

线程死锁的解决

/**
 * 定义化妆的方法
 */
public void doMakeup() {
    if (this.flag == 0) {
        /**
         * 同一个方法里,synchronized嵌套synchronized容易发生死锁。
         * 但同一个方法里,可以并行使用多个synchronized。
         */
        synchronized (lipstick) {   //synchronized (lipstick):设置口红为对象锁,当我拿着口红,别人就不能拿口红了。
            System.out.println(this.girlName + " 拿着口红");
            try {
                Thread.sleep(1000); //使线程休眠一秒
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
​
        synchronized (mirror) {
            System.out.println(this.girlName + " 拿着镜子");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    } else {
        synchronized (mirror) {
            System.out.println(this.girlName + " 拿着镜子");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
​
        synchronized (lipstick) {
            System.out.println(this.girlName + " 拿着口红");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

以上代码,输出结果为:

小红 拿着口红
小美 拿着镜子
小红 拿着镜子
小美 拿着口红

同一个方法里,可以并行使用多个synchronized。

但同一个方法里,synchronized嵌套synchronized容易发生死锁。

8、线程通信

线程通信(也叫线程并发协作)。

生产者/消费者模式

多线程环境下,我们经常需要多个线程的并发和协作。这个时候,就需要了解一个重要的多线程并发协作模型“生产者/消费者模式”。

特点:在线程协作时,可以实现线程与线程之间双耦合的设计。

角色介绍

生产者 指的是负责生产数据的模块(这里模块可能是:方法、对象、线程、进程)。

消费者 指的是负责处理数据的模块(这里模块可能是:方法、对象、线程、进程)。

消费者不能直接使用生产者的数据,它们之间有个“缓冲区”。生产者将生产好的数据放入“缓冲区”,消费者从“缓冲区”拿要处理的数据。

缓冲区 是实现并发的核心,缓冲区的设置有两个好处:

  • 实现线程的并发协作。

    有了缓冲区以后,生产者线程只需要往缓冲区里面放置数据,而不需要管消费者消费的情况;同样,消费者只需要从缓冲区拿数据处理即可,也不需要管生产者生产的情况。 这样,就从逻辑上实现了“生产者线程”和“消费者线程”的分离,解除了生产者与消费者之间的耦合。

  • 解决忙闲不均,提高效率。

    生产者生产数据慢时,缓冲区仍有数据,不影响消费者消费;消费者处理数据慢时,生产者仍然可以继续往缓冲区里面放置数据 。

线程并发协作的实现
实例21:创建缓冲区
package com.lg.sync;
​
/**
 * ProduceThread
 * 线程并发协作_创建缓冲区:实现生产者与消费者模式
 * wait():使线程等待。
 * 语法:该方法必须要在synchronized块中使用。
 * wait执行后,线程会将持有的对象锁释放,并进入阻塞状态。
 * 使得其他需要改对象锁的线程就可以继续运行了。
 *notify():唤醒线程。
 * 语法:该方法必须要在synchronized块中使用。
 * 该方法会唤醒处于等待状态队列中的一个线程。
 */
​
/**
 * 定义馒头类
 */
class ManTou {
    private int id;
​
    public int getId() {
        return id;
    }
​
    public void setId(int id) {
        this.id = id;
    }
}
​
/**
 * 定义缓冲区
 */
class SyncStack {
    //定义一个存放馒头的盒子
    private ManTou[] mt = new ManTou[10];
    //定义一个操作盒子的索引
    private int index;
​
    /**
     * 放馒头
     */
    synchronized public void push(ManTou manTou) {   //需要一个值
        //判断盒子是否已满
        while (this.index==this.mt.length) {   //如果馒头数等于盒子数
            try {
                /**
                 * wait():等待。
                 * 语法:该方法必须要在synchronized块中使用。
                 * wait执行后,线程会将持有的对象锁释放,并进入阻塞状态。
                 * 使得其他需要改对象锁的线程就可以继续运行了。
                 */
                this.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        this.notify();
        this.mt[this.index] = manTou;   //放馒头
        this.index++;   //累加
​
    }
​
    /**
     * 取馒头
     */
    synchronized public ManTou pop() {     //返回一个值
        while (this.index == 0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        /**
         * notify():唤醒生产馒头的线程。
         * 语法:该方法必须要在synchronized块中使用。
         * 该方法会唤醒处于等待状态队列中的一个线程。
         */
        this.notify();
        this.index--;
        return this.mt[this.index];
    }
}
​
/**
 * 定义生产者线程类
 */
class ShengChan extends Thread {
    //创建缓冲区
    private SyncStack ss;
​
    public ShengChan(SyncStack ss) {
        this.ss = ss;
    }
​
    @Override
    public void run() {
        for (int i = 1; i < 4; i++) {
            ManTou mt = new ManTou();   //创建馒头对象
            mt.setId(i);     //设置id,做标记
            System.out.println("生产馒头:"+mt.getId());
            this.ss.push(mt);   //调用生产馒头方法
        }
    }
}
​
/**
 * 定义消费者线程类
 */
class XiaoFei extends Thread {
    //创建缓冲区
    private SyncStack ss;
​
    public XiaoFei(SyncStack ss) {
        this.ss = ss;
    }
​
    @Override
    public void run() {
        for (int i = 1; i < 4; i++) {
            ManTou mt = this.ss.pop();  //创建馒头对象,调用取馒头方法
            System.out.println("取馒头"+mt.getId());
        }
    }
}
​
public class TestProduceThread {
    public static void main(String[] args) {
        //创建缓冲区
        SyncStack ss = new SyncStack();
        //创建生产者和消费者线程
        new ShengChan(ss).start();
        new XiaoFei(ss).start();
    }
}

以上代码,输出结果为:

生产馒头:1
生产馒头:2
生产馒头:3
取馒头3
取馒头2
取馒头1
线程通信总结

生产者和消费者模式:

  1. 生产者和消费者共享同一个资源,并且生产者和消费者之间相互依赖,互为条件。

  2. 对于生产者,没有生产产品之前,消费者要进入等待状态。而生产了产品之后,又需要马上通知消费者消费。

  3. 对于消费者,在消费之后,要通知生产者已经消费结束,需要继续生产新产品以供消费。

  4. 在生产者消费者问题中,仅有synchronized是不够的。synchronized可阻止并发更新同一个共享资源,实现了同步但是synchronized不能用来实现不同线程之间的消息传递(通信)。

  5. 那线程是通过哪些方法来进行消息传递(通信)的呢?见如下总结:

    方法名作 用
    final void wait()表示线程一直等待,直到得到其它线程通知
    void wait(long timeout)线程等待指定毫秒参数的时间
    final void wait(long timeout,int nanos)线程等待指定毫秒、微秒的时间
    final void notify()唤醒一个处于等待状态的线程
    final void notifyAll()唤醒同一个对象上所有调用wait()方法的线程,优先级别高的线程优先运行
  6. 以上方法均是java.lang.Object类的方法;都只能在同步方法或者同步代码块中使用,否则会抛出异常。

在实际开发中,尤其是“架构设计”中,会大量使用这个模式。 对于初学者了解即可,如果晋升到中高级开发人员,这就是必须掌握的内容。

9、线程组与线程池

线程组

线程组存在的意义,首要原因是安全

java默认创建的线程都是属于系统线程组,而同一个线程组的线程是可以相互修改对方的数据的。但如果在不同的线程组中,那么就不能“跨线程组”修改数据,可以从一定程度上保证数据安全.

线程池

线程池存在的意义,首要作用是效率

线程的创建和结束都需要耗费一定的系统时间(特别是创建),不停创建和删除线程会浪费大量的时间。所以,在创建出一条线程并使其在执行完任务后不结束,而是使其进入休眠状态,在需要用时再唤醒,那么就可以节省一定的时间。如果这样的线程比较多,那么就可以使用线程池来进行管理,保证效率。

线程组和线程池共有的特点
  • 都是管理一定数量的线程。

  • 都可以对线程进行控制:包括休眠,唤醒,结束,创建,中断(暂停),但并不一定包含全部这些操作。

9、线程安全

线程安全概述

在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且准确的执行,不会出现数据污染等意外情况。换言之,线程安全就是某个函数在并发环境中调用时,能够处理好多个线程之间的共享变量,是程序能够正确执行完毕。也就是说我们想要确保在多线程访问的时候,我们的程序还能够按照我们的预期的行为去执行,那么就是线程安全了。

当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象就是线程安全的。——《Java并发编程实践》

线程安全问题

要考虑线程安全问题,就需要先考虑Java并发的三大基本特性原子性可见性以及有序性

原子性

原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,即不被中断操作,要不全部执行完成,要不都不执行。就好比转账,从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。2个操作必须全部完成。

那程序中原子性指的是最小的操作单元,比如自增操作,它本身其实并不是原子性操作,分了3步的,包括读取变量的原始值、进行加1操作、写入工作内存。所以在多线程中,有可能一个线程还没自增完,可能才执行到第二部,另一个线程就已经读取了值,导致结果错误。那如果我们能保证自增操作是一个原子性的操作,那么就能保证其他线程读取到的一定是自增后的数据。

这一点,跟数据库事务的原子性概念差不多,即一个操作(有可能包含有多个子操作)要么全部执行(生效),要么全部都不执行(都不生效)。

关于原子性,一个非常经典的例子就是银行转账问题:比如A和B同时向C转账10万元。如果转账操作不具有原子性,A在向C转账时,读取了C的余额为20万,然后加上转账的10万,计算出此时应该有30万,但还未来及将30万写回C的账户,此时B的转账请求过来了,B发现C的余额为20万,然后将其加10万并写回。然后A的转账操作继续——将30万写回C的余额。这种情况下C的最终余额为30万,而非预期的40万。

可见性

可见性是指,当多个线程并发访问同一个变量时,一个线程修改修改了这个变量的值,其它线程能够立即看得到修改的值。

若两个线程在不同的cpu,那么线程1改变了i的值还没刷新到主存,线程2又使用了i,那么这个i值肯定还是之前的,线程1对变量的修改线程没看到这就是可见性问题。

有序性

程序执行的顺序按照代码的先后顺序执行,在多线程编程时就得考虑这个问题。

如何实现线程安全
1、互斥同步

何谓同步?在多线程编程中,同步就是一个线程进入监视器(可以认为是一个只允许一个线程进入的盒子),其他线程必须等待,直到那个线程退出监视器为止。

在实现互斥同步的方式中,最常使用的就是Synchronized 关键字

synchronized实现同步的基础就是:Java中的每一个对象都可以作为锁

具体表现为:

1.普通同步方法,锁是当前实例对象(this)。

2.静态同步方法,锁是当前类的Class对象。

3.同步方法块,锁是Synchronized括号里匹配的对象(自定义对象)。

如何实现?

synchronized经过编译之后,会在同步块的前后生成 monitorentermonitorexit这两个字节码指令。这两个字节码指令之后有一个reference类型(存在于java虚拟机栈的局部变量表中,可以根据reference数据,来操作堆上的具体对象)的参数来指明要锁定和解锁的对象。根据虚拟机规范,在执行monitorenter 指令时,首先会尝试获取对象的锁,如果该对象没有被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加一。若获取对象失败,那当前线程就要阻塞等待,知道对象锁被另一个线程释放。

synchronized用的锁是存放在对象头里面的,在jdk1.6之后,锁一共有四种状态:无锁状态,偏向锁状态(在对象头和栈帧中的锁记录里存储偏向锁的线程id),轻量级锁状态(将对象头的mark word复制到当前线程的栈帧的锁记录中,使用CAS操作将对象头中的markWord指向栈帧中的锁记录,如果成功,则线程就拥有了对象的锁。如果两个以上的线程争用一把锁的话,则膨胀为重量级锁),重量级锁状态。

因为之前我一直都很迷惑,所以我接下来讲一讲这一方面 :

大家应该都知道,java 在虚拟机中除了线程计数器java虚拟机栈是线程私有的,其余的java堆方法区,和运行时常量池都是线程共享的内存区域。java堆是存储对象和数组的,但是对象在内存中的存储布局可以分为三块区域:对象头实例数据(对象真正存储的有效信息,程序代码中所定义的各个类型的字段内容),对齐填充

为什么说synchronized的锁是存放在对象头里面呢?因为对象头里面也存储了两部分信息:第一部分呢,存储对象自身的运行时数据,包括哈希码,GC分代年龄,锁状态标识位,线程持有的锁,偏向锁Id,偏向时间戳等数据。第二部分是类型指针,虚拟机通过这个来确定该对象是哪个类的实例。

如何判断该对象有没有被锁?对象头里面锁状态的标志位会发生变化,当其他线程查看synchronized 锁定的对象时,会查看该对象的对象头的标志位有没有发生变化,若标志位为01,则表示未锁定,为00时,则表示轻量级锁定,为10时,则为重量级锁定状态。为01时,则为偏向锁,为11时,则为GC标记状态。

除了synchronized 关键字之外,还可以使用JUC包下的重入锁来实现同步 。

2、非阻塞同步

因为使用synchronized的时候,只能有一个线程可以获取对象的锁,其他线程就会进入阻塞状态,阻塞状态就会引起线程的挂起和唤醒,会带来很大的性能问题,所以就出现了非阻塞同步的实现方法。

先进行操作,如果没有其他线程争用共享数据,那么操作就成功了,如果共享数据有争用,就采取补偿措施(不断地重试)。

我们想想哈,互斥同步里实现了 操作的原子性(这个操作没有被中断) 和 可见性(对数据进行更改后,会立马写入到内存中,其他线程在使用到这个数据时,会获取到最新的数据),那怎么才能不用同步来实现原子性和可见性呢?

CAS是实现非阻塞同步的计算机指令,它有三个操作数:内存位置旧的预期值新值,在执行CAS操作时,当且仅当内存地址的值符合旧的预期值的时候,才会用新值来更新内存地址的值,否则就不执行更新。

使用方法:使用JUC包下的整数原子类decompareAndSet()和getAndIncrement()方法。

缺点 :ABA 问题 版本号来解决。

只能保证一个变量的原子操作,解决办法:使用AtomicReference类来保证对象之间的原子性。可以把多个变量放在一个对象里。

3、无同步方案

线程本地存储:将共享数据的可见范围限制在一个线程中。这样无需同步也能保证线程之间不出现数据争用问题。

经常使用的就是ThreadLocal类

ThreadLocal类 最常见的ThreadLocal使用场景为用来解决数据库连接Session管理等。

其实引起线程不安全最根本的原因就是 :线程对于共享数据的更改会引起程序结果错误。

线程安全的解决策略就是:保护共享数据在多线程的情况下,保持正确的取值。

10、线程相关类

Java还为线程安全提供了一些工具类,如ThreadLocal类,它代表一个线程局部变量,通过把数据放在ThreadLocal中就可以让每个线程创建一个该变量的副本,从而避免并发访问线程安全问题。除此之外,Java5还新增了大量的线程安全类。

1.ThreadLocal类

早在JDK1.2推出之时,Java就为多线程编程提供了一个ThreadLocal类;从Java5.0以后,Java引入了泛型支持,Java为该ThreadLocal类增加了泛型支持,即ThreadLocal。通过使用ThreadLocal类可以简化多线程编程时的并发访问,使用这个工具类可以简捷地隔离多线程程序的竞争资源。功用其实非常简单,就是为每个使用该变量的线程都提供一个变量值的副本,使每一个线程都可以独立地改变自己的副本,而不会和其他线程的副本冲突。从线程的角度看,就好像每一个线程都可以完全拥有该变量一样。 ThreadLocal类的用法非常简单,它只提供了如下三个public方法

  • T get():返回此线程局部变量中当前线程副本中的值。

  • void remove():删除此线程局部变量中当前线程的值。

  • void set(T value):设置此线程局部变量中当前线程副本中的值。

ThreadLocal和其他所有的同步机制一样,都是为了解决多线程中对同一变量的访问冲突,在普通的同步机制中,是通过对象加锁来实现多个线程对同一变量的安全访问的。该变量是多个线程共享的,所以需要使用这种同步机制,需要很细致地分析在什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放该对象的锁等。在这种情况下,系统并没有将者份资源复制多份,只是采用了安全机制来控制对这份资源的访问而已。 ThreadLocal从另一个角度来解决多线程的并发访问,ThreadLocal将需要并发访问的资源复制多份,每个线程拥有一份资源,每个线程都拥有自己的资源副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把线程不安全的整个变量封装进ThreadLocal,或者把该对象与线程关联的状态使用ThreadLocal。 ThreadLocal并不能替代同步机制,两者面向的问题领域不同。同步机制是为了同步多个线程对相同资源的并发访问,是多个线程之间进行通信的有效方式;而ThreadLocal是为了隔离多个线程的数据共享,从根本上避免多个线程之间对共享资源(变量)的竞争,也就不需要对多个线程进行同步了。

2.包装线程不安全的集合

Java集合中ArrayList,LinkedList,HashSet,TreeSet,HashMap,TreeMap等都是线程不安全的,也就是说,当多个线程并发向这些集合中存,取元素时,就可能会破坏这些集合数据完整性。 如果程序中有多个线程可能访问以上这些集合,就可以使用Collections提供的类方法把这些集合包装成线程安全的集合。Collections提供了如下几个静态方法。

img

3.线程安全的集合类

实际上从Java5开始,在java.util.concurrent包下提供了大量支持高并发并访问的集合接口和实现类:

img

从上图可以看出,这些线程安全的集合类可分为如下两类

  • 以Concurrent开头的集合类,如ConcurrentHashMap,ConcurrentSkipListMap,ConcurrentSkipListSet,ConcurrentLinkedQueue和ConcurrentLinkedDeque。

  • 以CopyOnWrite开头的集合类,如CopyOnWriteArrayList,CopyOnWriteArraySet。

其中以Concurrent开头的集合代表支持并发访问的集合,它们可以支持多个线程并发写入访问,这些写入线程的所有操作都是线程安全的,但是读取操作不必锁定。以Concurrent开头的集合类采用了更负载的算法来保证永远不会锁住整个集合,因此在并发写入时有较好的性能。

当多个线程共享访问一个公共集合时,ConcurrentLinkedQueue是一个恰当的选择。ConcurrentLinkedQueue不允许使用null元素。ConcurrentLinkedQueue实现了多线程的高效访问,多线程访问ConcurrentLinkedQueue集合时无须等待。

在默认情况下,ConcurrentHashMap 支持16个线程并发写入,当有超过16个线程并发向该Map中写入数据时,可能有一些线程需要等待。实际上,程序通过设置concurrentLevel构造参数(默认值为16)来支持更多的并发写入线程。 与前面介绍的HashMap和普通集合不同的时,因为ConcurrentLinkedQueue和ConcurrentHashMap支持多线程并发访问,所以当使用迭代器来遍历元素集合时,该迭代器可能不能反映出迭代器之后所做的修改。但程序不会抛出任何异常。

Java8扩展了ConcurrentHashMap的功能,Java8为该类新增了30多个新方法,这些方法可借助于Stream和Lambda表达式支持执行聚集操作。ConcurrentHashMap新增的方法大致可分如下三类。

  • forEach 系列

  • search 系列

  • reduce 系列

由于CopyOnWriteArraySet 的底层封装了CopyOnWriteArrayList,因此它的实现机制完全类似于CopyOnWriteArrayList集合。

对于CopyOnWriteArrayList集合,正如它的名字所暗示,它采用复制底层数据的方式来实现写操作。

当线程对CopyOnWriteArrayList集合执行读取操作时,线程将会直接读取集合本身,无须加锁与阻塞。当线程对CopyOnWriteArrayList集合执行写入操作时(包括add(),remove(),set()等方法),该集合会在底层复制一份新的数组,接下来对新的数组执行写入操作。由于对CopyOnWriteArrayList集合的写入操作都是对数组的副本执行操作,因此它是线程安全的。需要指出的是,由于CopyOnWriteArrayList执行写入操作需要频繁地复制数组,性能比较差,但是由于读操作与写操作不是操作同一个数组,而且读操作也不需要加锁,因此读操作就很快,很安全。由此可见,CopyOnWriteArrayList适合用在读取操作远远大于写操作的场景中,例如缓存等。

线程相关类内容取自——《疯狂Java讲义-李刚》书中多线程章节。

区别

线程与进程的区别

根本区别:线程是程序执行(处理器任务调度和执行)的最小单位,而进程是操作系统分配资源的最小单位。

资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。

包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的。

调度和切换:线程上下文切换比进程上下文切换要快得多。

影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行。

线程与方法的区别

方法的执行特点:串行。

线程的执行特点:并行。

并发与并行的区别

并发是指两个程序或以上在同一时间段上发生。

并行是指两个程序或以上在同一时刻上发生(同时发生)。

并发是一个人做多件事,来回切换。

并行是多个人做多件事,互不干涉。

同步与互斥的区别

互斥:是指散布在不同进程之间的若干程序片段,当某个进程执行其中的一个程序片段时,其他进程就不能运行它们之中的任一程序片段,只能等到该进程运行完之后才可以继续运行。

同步:是指散布在不同进程之间的若干程序片段,它们的运行必须严格按照一定的先后次序来运行,这种次序依赖于要完成的任务。比如数据的收发,必须发送方发送了接收方才能收。

互斥是两个进程或线程之间不可以同时运行,它们会互相排斥,必须等待其中的一个运行完,另一个才可以运行。而同步也是不可以同时运行,并且还需要按照某种顺序来运行。

区别

  • 互斥是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法控制对资源的访问顺序

  • 同步是指在互斥的基础上实现对资源的有序访问

run() 与 start() 的区别

start:用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码。通过调用Thread类的 start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法,这里方法 run()称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。

run:run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。

总结:调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程里执行。

记住:多线程就是分时利用 CPU,宏观上让所有线程一起执行 ,也叫并发

Java 的线程是通过 java.lang.Thread 类来实现的。JVM 启动时会有一个由主方法所定义的线程。可以通过创建 Thread 的实例来创建新的线程。每个线程都是通过某个特定 Thread 对象所对应的run() 方法来完成其操作的,run() 方法称为线程体。通过调用 Thread 类的 start() 方法来启动一个线程。

ThreadLocal与同步机制的区别

ThreadLocal 并不能替代同步机制,两者面向的问题不同。

同步机制是为了同步多个线程对相同资源的并发访问,是多个线程之间进行通信的有效方式。

ThreadLocal 是为了隔离多个线程的数据共享,从根本上避免多个线程之间对共享资源的竞争,也就不需要对多个线程进行同步了。

通常建议:如果多个线程之间需要共享资源,以达到线程之间的通信功能,就使用同步机制;如果仅仅需要隔离多个线程之间的共享冲突,则可以使用 ThreadLocal。

*以上多线程的内容仅供个人学习使用,如有不对,请多多指正。*

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

狸猫丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值