Fork me on GitHub

Java 多线程

注意:所有文章除特别说明外,转载请注明出处.

多线程之传统多线程

[TOC]

1.传统线程技术

1.进程:正在运行的程序,负责了这个程序的内存空间分配,代表了内存中的执行区域。

2.线程:在一个进程中负责一个执行路径。3.多线程:在一个进程中多个执行路径同时执行。

总结:进程负责一个程序的内存空间分配,线程负责一个程序的执行路径。进程是资源分配的最小单位,线程是CPU调度的最小单位。

1.所有与进程相关的资源都被记录在PCB中

2.进程是抢占处理机的调度单位,线程属于某个进程,共享其资源

    1.线程和进程的区别:

        1.线程不能看作独立应用,而进程可看作独立应用

        2.进程有独立的地址空间,相互不影响,线程只是进程的不同执行路径

        3.线程没有独立的地址空间,多进程的程序比多线程程序健壮

        4.进程的切换比线程的切换开销大


    2.Java进程和线程的关系

        1.Java对操作系统提供的功能进行封装,包括进程和线程

        2.运行一个程序会产生一个进程,进程包含至少一个线程

        3.每个进程对应一个JVM实例,多个线程共享JVM里的堆

        4.Java采用单线程编程模型,程序会自动创建主线程

        5.主线程可以创建子线程,原则上要后于子线程完成执行

提示:再次我们了解到,由于cpu的分时机制,使得每个进程都能够循环获得自己的cpu时间片。但因为轮换速度非常快,所以在我们看来所有的程序好像在同时运行一样。

多线程的优势:1.解决一个进程里面可以同时运行多个任务。2.提高资源的利用率,不是提高效率。

多线程的弊端:1.降低一个进程里面的线程的执行频率。2.对线程进行管理要求额外的CPU开销。线程的使用会给系统带来上下文切换的额外负担。3.公有变量的同时读或写。当多个线程需要对公有变量进行写操作时,后一个线程往往会修改掉前一个线程存放的数据,发生线程安全问题。4.线程的死锁。即较长时间的等待或资源竞争以及死锁等多线程症状。

1.线程的优先级
Java线程的优先级是整数,取值范围是1-10,默认情况下,每个线程都会分配到一个优先级(5)。
2.Thread方法
1.start() 方法 Java虚拟机调用该线程的run()方法

2.run() 方法 

3.setName()方法 改变线程的名称

4.

2.传统线程的创建方式

1.继承Thread类,覆盖run()方法。
//在此demo中,线程1 2 只是无序的执行,多线程资源争夺没有起到效果
class Demo extends Thread{
    public Demo(String name){
        super(name);
    }

    public void print(){
        for(int i=0;i<10;i++){
            //这里的getName()方法是获取线程的名字
            System.out.println(this.getName()+":"+i);
        }
    }
    public static void main(String[] args){
        Demo demo1 = new Demo("Aaron");//创建线程1
        Demo demo2 = new Demo("Brian");//创建线程2
        demo1.print();
        demo2.print();
    }
}

2.把要执行的任务放在run()方法中
//此程序中线程是按照顺序进行的,即先运行了线程1,然后在运行线程2,没有进行线程之间的争夺效果
class Demo extends Thread {
    @Override
    public void run(){
        print();//该程序主要的任务就是运行print()方法,所以在run()方法中调用print方法即可。
    }

    public Demo (String name){
        super(name);
    }

    public void print(){
        for(int i=0;i<10;i++){
            System.out.println(this.getName()+":"+i);
        }
    }

    public static void main(String[] args){
        Demo demo1 = new Demo("Aaron");//创建线程1
        Demo demo2 = new Demo("Brian");//创建线程2

        //运行两个线程
        demo1.run();
        demo2.run();
    }
}

3.调用start()方法启动线程
//此demo中实现了线程之间的资源争夺过程
class Demo extends Thread {
    @Override
    public void run(){
        print();//该程序主要的任务就是运行print()方法,所以在run()方法中调用print方法即可。
    }

    public Demo(String name){
        super(name);
    }
    public void print(){
        for(int i = 0; i < 10; i++){
            try{
                this.sleep(1000);//此运行线程睡眠1s
            }catch(InterruptedException e){
                e.printStackTrace();
            }
            System.out.println(this.getName()+":"+i);
        }
    }
    public static void main(String[] args){
        Demo demo1 = new Demo("Aaron");//创建线程1
        Demo demo2 = new Demo("Brian");//创建线程2
        demo1.start();
        demo2.start();
    }
}

总结:1.线程的启动使用了父类的start()方法。2.如果线程对象直接调用run()方法,那么Java虚拟机不会将其当做线程来运行,只会当做普通的方法调用。3.线程的启动只能有一次,否则会抛出异常。4.可以直接创建Thread类的对象并启动该线程,但是如果没有重写run(),什么也不执行。5.匿名内部类的线程实现方式。


2.实现Runnable接口

实现Runnable接口是实现线程最简单的方式,实现Runnable,一个类只需要执行一个方法调用run()就行。

package cn.edu.xidian.B.Demo.Thread;

public class RunnableDemo implements Runnable {
    private Thread t;
    private String tname;

    public RunnableDemo(String tname) {
        this.tname = tname;
        System.out.println("创建" + tname);
    }

    @Override
    public void run() {
        System.out.println("运行" + tname);
        try {
            for (int i = 4; i > 0; i--){
                System.out.println("线程" + tname + "," + i);
                Thread.sleep(100);
            }
        } catch (InterruptedException e) {
            System.out.println("线程" + tname + "暂停" );
        }
        System.out.println("线程" + tname + "结束");
    }

    public void start(){
        System.out.println("开始" + tname);
        if (t == null){
            t = new Thread(this,tname);
            t.start();
        }
    }
}

//测试类
package cn.edu.xidian.B.Demo.Thread;

public class ThreadRunnableTest {
    public static void main(String[] args) {
        RunnableDemo r1 = new RunnableDemo("线程1");
        r1.start();

        RunnableDemo r2 = new RunnableDemo("线程2");
        r2.start();

    }
}
3.Thread和Runnable的关系
1.Thread是实现了Runnable接口的类,使得run支持多线程

2.因类的单一继承原则,推荐多使用Runnable接口

J.U.C - 其它组件

通过 Callable 和 Future 创建线程

Java 5 之后,Java提供Callable接口,该接口是Runnable接口的增强,Callable接口提供一个call()方法可以作为线程执行体,但是call()方法比run()方法更加强大。

因此我们可以提供一个Callable对象作为Thread的target,而该线程的线程执行体就是该Callable对象的call()方法。

call()方法并不是直接调用,它是作为线程执行体被调用的。

Java 5 提供了Future接口来代表Callable接口里call()方法的返回值,并为Future接口提供了一个FutureTask实现类,可以作为Thread类的target。

我们知道Callable接口在调用时有返回值,返回值通过Future进行封装。FutureTask实现了RunnableFuture接口,该接口继承自 Runnable 和 Future 接口,这使得 FutureTask 既可以当做一个任务执行,也可以有返回值。

FutureTask 可用于异步获取执行结果或取消执行任务的场景。当一个计算任务需要执行很长时间,那么就可以用 FutureTask 来封装这个任务,主线程在完成自己的任务之后再去获取结果。

package Java.ChapterSeven.FutureTask;

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

public class FutureTaskExample {

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        //用于异步获取执行结果或取消执行任务的场景
        //当一个任务需要很久执行时间,可以用futureTask来封装这个任务,主线程完成自己的任务之后再去获取结果
        FutureTask<Integer> futureTask = new FutureTask<Integer>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int result = 0;
                for (int i = 0; i < 100; i++){
                    Thread.sleep(10);
                    result += i;
                }
                return result;
            }
        });

        Thread computeThread = new Thread(futureTask);
        computeThread.start();

        Thread otherThread = new Thread(() -> {
            System.out.println("other task is running...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        otherThread.start();
        System.out.println(futureTask.get());
    }
}

other task is running...
4950

提示:实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。

1.创建并启动有返回值的线程步骤:
1. 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。call()方法可以抛出异常。

2. 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。

3. 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。

4. 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。


package cn.edu.xidian.B.Demo.Thread;

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

public class CallableThreadTest implements Callable<Integer> {
    public static void main(String[] args) {

        //1.程序首先创建一个Callable实现类的实例
        CallableThreadTest ctt = new CallableThreadTest();

        //2.然后将该实例包装成一个FutureTask对象
        FutureTask<Integer> ft = new FutureTask<Integer>(ctt);

        for (int i = 0; i < 100; i++){
            System.out.println(Thread.currentThread().getName() + "的循环变量i的值" + i);
            if (i == 20){
                new Thread(ft,"有返回值的线程").start();
            }
        }

        try {

            //3.调用FutureTask对象的get()方法返回call()方法的返回值,该方法将导致线程阻塞,直到call()方法结束并返回为止
            System.out.println("子线程的返回值:" + ft.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

    @Override
    public Integer call() throws Exception {
        int i = 0;
        for (; i < 100; i++){
            System.out.println(Thread.currentThread().getName() + "" + i);
        }
        return i;
    }
}


1.程序首先创建一个Callable实现类的实例

2.然后将该实例包装成一个FutureTask对象

3.调用FutureTask对象的get()方法返回call()方法的返回值,该方法将导致线程阻塞,直到call()方法结束并返回为止

总结:1.采用实现 Runnable、Callable 接口的方式创建多线程时,线程类只是实现了 Runnable 接口或 Callable 接口,还可以继承其他类。

2.使用继承 Thread 类的方式创建多线程时,编写简单,如果需要访问当前线程,则无需使用 Thread.currentThread() 方法,直接使用 this 即可获得当前线程。


2.线程的状态

1.新建状态 使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。

2.运行状态

    1.就绪状态: 当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。

    2.可运行:线程对象创建后,其他线程调用了该对象的start()方法。该状态的线程位于可运行线程池中,变得可运行,等待获取cpu的执行权。

3.无限期等待 

    不会被分配CPU执行时间,需要显式被唤醒

4.限期等待 

    在一定时间之后会由系统自动唤醒

5.阻塞

    等待获取排它锁

    阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。sleep()睡眠。

6.死亡:线程执行完它的任务时。

注意:在启动线程的时候使用start()方法,永远不要调用run()方法,调用start()方法来启动线程,系统会将该run()方法当做线程执行体来处理。如果直接调用线程对象的run()方法,则run()方法就会立即被执行,在run()方法返回之前,其它线程无法并发执行。

提示:如果希望调用子线程的start()方法之后,子线程立即执行,程序可以使用 Thread.sleep()方法睡眠一秒,此时CPU不会空闲,它回去执行另一个处于就绪状态的线程。

提示:不要试图对一个已经死亡的线程调用start()方法使他重新启动,死亡就是死亡,该线程将不可再次作为线程执行。只能对新建状态的线程调用start()方法,调用两次也是错误的。会引起IllegalThreadStateException异常。

3.常见线程的方法

1.Thread(String name) 初始化线程的名字
2.getName() 返回线程的名字
3.setName(String name) 设置线程对象名
4.getPriority() 返回当前线程对象的优先级 默认线程的优先级是5
5.setPriority(int newPriority) 设置线程的优先级 虽然设置了线程的优先级,但是具体的实现取决于底层的操作系统的实现(最大的优先级是10 ,最小的1 ,默认是5)。
6.currentThread() 返回CPU正在执行的线程的对象

程序:

class ThreadDemo1 extends Thread {
    public ThreadDemo1(){

    }
    public ThreadDemo1( String name ){
       super( name );
    }

    public void run(){
       int i = 0;
       while(i < 30){
          i++;
          System.out.println( this.getName() + " "+ " : i = " + i);
          System.out.println( Thread.currentThread().getName() + " "+ " : i = " + i);
          System.out.println( Thread.currentThread() == this );
          System.out.println( "getId()" + " "+ " : id = " + super.getId() );
          System.out.println( "getPriority()" + " "+ " : Priority = " + super.getPriority() );
       }
    }
}
class Demo3 
{
    public static void main(String[] args) 
    {
        ThreadDemo1 th1 = new ThreadDemo1("线程1");
        ThreadDemo1 th2 = new ThreadDemo1("线程2");
        // 设置线程名
        th1.setName( "th1" );
        th2.setName( "th2" );
        // 设置线程优先级  1 ~ 10
        th1.setPriority( 10 ); 
        th2.setPriority( 7 ); 
        // 查看SUN定义的线程优先级范围
        System.out.println("max : " + Thread.MAX_PRIORITY );
        System.out.println("min : " + Thread.MIN_PRIORITY );
        System.out.println("nor : " + Thread.NORM_PRIORITY );
        th1.start();
        th2.start();
        System.out.println("Hello World!");
    }
}
案例:模拟卖票问题,也就是一个线程的资源争夺问题
class SaleTickets extends Thread {
    int tickets = 100;
    public void run(){
        while(tickets>0){
            System.out.println("卖到了第"+tickets+"张票");
            tickets--;
        }
    }
}

class Demo {
    public static void main(String[] args){
        SaleTickets thread1 = new SaleTickets();
        SaleTickets thread2 = new SaleTickets();
        SaleTickets thread3 = new SaleTickets();
        SaleTickets thread4 = new SaleTickets();
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

在这个demo中需要注意的是我们启动了四个线程,那么tickets是一个成员变量,在一个线程对象中都维护了属于自己的tickets属性,所以总的存在四份。解决方案:成员变量tickets使用static修饰,使得每一个线程都共享一份属性。

class SaleTickets extends Thread {
    static int tickets = 100;
    public void run(){
        while(tickets>0){
            System.out.println("卖到了第"+tickets+"张票");
            tickets--;
        }
    }
}

class Demo {
    public static void main(String[] args){
        SaleTickets thread1 = new SaleTickets();
        SaleTickets thread2 = new SaleTickets();
        SaleTickets thread3 = new SaleTickets();
        SaleTickets thread4 = new SaleTickets();
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

2.实现Runnable接口,Runnable不是线程,是线程运行的代码的宿主。
    1.Runnable,target,run,start之间的关系:
        1.Runnable是一个接口
        2.target是Thread类中类型为Runnable,名为target的属性
        3.run是Thread类实现了Runnable的接口,重写的方法
        4.start是启动线程的方法
        5.在Thread类中,调用关系:start->start0->run->target.run

注意:target属性由private void init(ThreadGroup g, Runnable target, String name,long stackSize,AccessControlContext acc)方法初始化。init方法在Thread类的构造方法里被调用。

案例:卖票
    class MyTicket implements Runnable {
    int tickets = 100;
    public void run() {
        while (true) {
            if (tickets > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "窗口@销售:"
                        + tickets + "号票");
                tickets--;

            } else {
                System.out.println("票已卖完。。。");
                break;
            }
        }
    }
}
public class Demo6 {
    public static void main(String[] args) {
        MyTicket mt = new MyTicket();
        Thread t1 = new Thread(mt);
        Thread t2 = new Thread(mt);
        Thread t3 = new Thread(mt);
        Thread t4 = new Thread(mt);
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

    2.匿名内部类对象的构造方法如何调用父类的非默认构造方法

2.传统定时器技术

如:

static int count = 0;
public static void main(String[] args) {

    class MyTimerTask extends TimerTask{

        @Override
        public void run() {
            System.out.println(Thread.currentThread()+" bomb!");
            new Timer().schedule(new MyTimerTask(), 2000+1000*(count++%2));
        }
    }
    //3s后开启定时器
    new Timer().schedule(new MyTimerTask(),3000);
}

提示:可以使用quarlz开源工具。

3.锁对象

每个Java对象都有一个锁对象,而且只有一把钥匙。
1.创建锁对象
可以使用this关键字作为锁对象,或者使用所在类的字节码文件对应的Class对象作为锁对象。

互斥:

关键字:synchronized,检查锁对象。

synchronized(this)
synchronized void function(){}
synchronized(A.class)

//锁对象的死锁案例
public class DeadLock {
    public static void main(String[] args) {
        new Thread(new Runnable() { // 创建线程, 代表中国人
                    public void run() {
                        synchronized ("刀叉") { // 中国人拿到了刀叉
                            System.out.println(Thread.currentThread().getName()
                                    + ": 你不给我筷子, 我就不给你刀叉");
                            try {
                                Thread.sleep(10);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            synchronized ("筷子") {
                                System.out.println(Thread.currentThread()
                                        .getName() + ": 给你刀叉");
                            }
                        }
                    }
                }, "中国人").start();
        new Thread(new Runnable() { // 美国人
                    public void run() {
                        synchronized ("筷子") { // 美国人拿到了筷子
                            System.out.println(Thread.currentThread().getName()
                                    + ": 你先给我刀叉, 我再给你筷子");
                            try {
                                Thread.sleep(10);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            synchronized ("刀叉") {
                                System.out.println(Thread.currentThread()
                                        .getName() + ": 好吧, 把筷子给你.");
                            }
                        }
                    }
                }, "美国人").start();
    }
}

总结:死锁表示进程A中包含资源A,进程B中包含资源B,A的下一步需要资源B,B的下一步需要资源A,所以它们就互相等待对方占有的资源释放,所以也就产生了一个循环等待死锁。

4.同步

1.要用到共同数据(包括同步锁)或共同算法的若干个方法应该归在同一个类身上,这种设计体现了高聚类和程序的健壮性。2.同步互斥不是在线程上实现,而是在线程访问的资源上实现,线程调用资源。

线程间通信其实就是多个线程在操作同一个资源,但操作动作不同,wait(),notify(),notifyAll()都使用在同步中,因为要对持有监视器(锁)的线程操作,所以要使用在同步中,因为只有同步才具有锁。

提示:1.wait()与sleep()之间的区别,wait()方法释放资源,释放锁,是Object的方法。而sleep()方法只释放资源,不释放锁,是Thread的方法。2.在定义了notify还要定义notifyAll是因为只用notify容易出现只唤醒本方线程情况,导致程序中的所有线程都在等待。

4.1 同步方法

1.同步函数

同步函数就是用synchronize关键字修饰方法,由于每一个Java对象都有一个内置锁,所以在用synchronize关键字修饰方法时内置锁会保护整个方法,在调用该方法之前,都需要首先获得内置锁,否则会处于阻塞状态。

public synchronized void run(){}

2.同步代码块

表示synchronized关键字修饰的语句块,被该关键字修饰的语句块会自动加上内置锁,从而实现同步。

public void run() {
    while(true){
        //同步代码块
        synchronized (this) {                                 
            if(tick>0){
                try {
                    //执行中让线程睡眠10毫秒
                    Thread.sleep(10);                                
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + " " + tick--);
            }
        }
    }
}

注意:因为静态的方法中不能定义this,所以在静态的同步方法中使用的锁是该方法所在类的字节码文件对象。类名.class

public static mySyn(String name){
    synchronized (Xxx.class) {
        Xxx.name = name;
    }
}
4.2 等待唤醒机制

1.wait:告诉当前线程放弃执行权,并放弃监视器(锁)并进入阻塞状态,直到其他线程持有获得执行权,并持有了相同的监视器(锁)并调用notify为止。
2.notify:唤醒持有同一个监视器(锁)中调用wait的第一个线程。例如,餐馆有空位置后,等候就餐最久的顾客最先入座。注意:被唤醒的线程是进入了可运行状态。等待cpu执行权。
3.notifyAll:唤醒持有同一监视器中调用wait的所有的线程。

解决生产者与消费者问题:

package cn.itcast.gz.runnable;

public class Demo10 {
    public static void main(String[] args) {
        Person p = new Person();
        Producer pro = new Producer(p);
        Consumer con = new Consumer(p);
        Thread t1 = new Thread(pro, "生产者");
        Thread t2 = new Thread(con, "消费者");
        t1.start();
        t2.start();
    }
}

// 使用Person作为数据存储空间
class Person {
    String name;
    String gender;
    boolean flag = false;

    public synchronized void set(String name, String gender) {
        if (flag) {
            try {
                wait();
            } catch (InterruptedException e) {

                e.printStackTrace();
            }
        }
        this.name = name;
        this.gender = gender;
        flag = true;
        notify();
    }

    public synchronized void read() {
        if (!flag) {
            try {
                wait();
            } catch (InterruptedException e) {

                e.printStackTrace();
            }
        }
        System.out.println("name:" + this.name + "----gender:" + this.gender);
        flag = false;
        notify();
    }

}

// 生产者
class Producer implements Runnable {
    Person p;

    public Producer() {

    }

    public Producer(Person p) {
        this.p = p;
    }

    @Override
    public void run() {
        int i = 0;
        while (true) {

            if (i % 2 == 0) {
                p.set("jack", "man");
            } else {
                p.set("小丽", "女");
            }
            i++;

        }

    }

}

// 消费者
class Consumer implements Runnable {
    Person p;

    public Consumer() {

    }

    public Consumer(Person p) {
        this.p = p;
    }

    @Override
    public void run() {

        while (true) {
            p.read();

        }
    }

}

如:子线程循环5次,主线程循环10次,如此交替50次
分析:使用一个Business类来包含子线程和主线程要运行的代码,从而,该类的对象成为加锁的对象。同步互斥在该类实现,由线程调用该类的方法,即调用了资源。

public class TraditionalThreadCommunication {
    public static void main(String[] args) {
        Business business = new Business();
        new Thread(
                new Runnable() {
                    @Override
                    public void run() {
                        for(int i=1;i<=50;i++){
                            business.sub(i);
                        }
                    }
                }
        ).start();

        for(int i=1;i<=50;i++){
            business.main(i);
        }

    }
}

class Business{
    private boolean bShouldSub = true;

    public synchronized void sub(int i){
        while(!bShouldSub){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        for(int j=1;j<=5;j++){
            System.out.println("sub thread count "+j+","+i+"/50");
        }
        bShouldSub = false;
        this.notify();
    }
    public synchronized void main(int i){
        while(bShouldSub){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        for(int j=1;j<=10;j++){
            System.out.println("main thread count "+j+","+i+"/50");
        }
        bShouldSub = true;
        this.notify();
    }
}

提示:判断条件时,while与if的区别在于,while防止伪唤醒。

4.3 线程的生命周期
//在多线程运行过程中,只要控制住循环,然后就可以让run()方法结束,线程结束
class StopThread implements Runnable {
    public boolean tag = true;
    @Override
    public void run() {
        int i = 0;

        while (tag) {
            i++;
            System.out.println(Thread.currentThread().getName() + "i:" + i);
        }
    }
}
public class Demo8 {
    public static void main(String[] args) {
        StopThread st = new StopThread();
        Thread th = new Thread(st, "线程1");
        th.start();
        for (int i = 0; i < 100; i++) {
            if (i == 50) {
                System.out.println("main i:" + i);
                st.tag = false;
            }
        }
    }
}

5.线程间通讯

线程的通讯指的是多个线程在操作同一个资源,但是操作的动作不同。

1.案例:生产者与消费者问题

提示:如有多个生产者和消费者,需要使用while循环判断标记,然后再使用notifyAll唤醒。否则只用notify容易出现只唤醒本方线程情况,导致程序中的所有线程都在等待。

如:有一个数据存储空间,划分为两个部分:1.存储人的姓名。2.存储性别。然后开启两个线程,一个作为不停的向其中存储姓名与性别(生产者),另外一个线程作为从数据存储空间中取出数据(消费者)。

所以在这个问题中需要考虑多线程的情况,如:1.假如生产者刚向数据存储空间中添加了一个人名,还没有来得及添加性别,cpu就切换到了消费者的线程,消费者就会将这个人的姓名和上一个人的性别进行了输出。2.生产者生产了若干次数据,消费者才开始取数据,或者消费者取出数据后,没有等到生产者放入新的数据,消费者又重复的取出自己已经取过的数据。

//main方法
public class Demo10 {
    public static void main(String[] args) {
        Person p = new Person();
        Producer pro = new Producer(p);
        Consumer con = new Consumer(p);
        Thread t1 = new Thread(pro, "生产者");
        Thread t2 = new Thread(con, "消费者");
        t1.start();
        t2.start();
    }
}

// 使用Person作为数据存储空间
class Person {
    String name;
    String gender;
}

// 生产者
class Producer implements Runnable {
    Person p;

    public Producer() {

    }

    public Producer(Person p) {
        this.p = p;
    }

    @Override
    public void run() {
        int i = 0;
        while (true) {
            if (i % 2 == 0) {
                p.name = "jack";
                p.gender = "man";
            } else {
                p.name = "小丽";
                p.gender = "女";
            }
            i++;
        }
    }
}

// 消费者
class Consumer implements Runnable {
    Person p;

    public Consumer() {

    }

    public Consumer(Person p) {
        this.p = p;
    }

    @Override
    public void run() {

        while (true) {
            System.out.println("name:" + p.name + "---gnder:" + p.gender);
        }
    }

}

注意:在上面程序输出的过程中出现了线程的安全问题,所以需要使用关键字 synchronized 来解决该问题。

package cn.itcast.gz.runnable;

public class Demo10 {
    public static void main(String[] args) {
        Person p = new Person();
        Producer pro = new Producer(p);
        Consumer con = new Consumer(p);
        Thread t1 = new Thread(pro, "生产者");
        Thread t2 = new Thread(con, "消费者");
        t1.start();
        t2.start();
    }
}

// 使用Person作为数据存储空间
class Person {
    String name;
    String gender;
}

// 生产者
class Producer implements Runnable {
    Person p;

    public Producer() {

    }

    public Producer(Person p) {
        this.p = p;
    }

    @Override
    public void run() {
        int i = 0;
        while (true) {
            synchronized (p) {
                if (i % 2 == 0) {
                    p.name = "jack";
                    p.gender = "man";
                } else {
                    p.name = "小丽";
                    p.gender = "女";
                }
                i++;
            }

        }

    }

}

// 消费者
class Consumer implements Runnable {
    Person p;

    public Consumer() {

    }

    public Consumer(Person p) {
        this.p = p;
    }

    @Override
    public void run() {

        while (true) {
            synchronized (p) {
                System.out.println("name:" + p.name + "---gnder:" + p.gender);
            }

        }
    }

}

//解决上面的问题2,生产者生产了若干次数据之后,消费者才开始取数据,或者消费者取出数据之后没有等到消费者//放入新的数据,消费者又重复取出自己已经取过的数据。
//解决:在Person类中添加set和read方法并设置为synchronized的,让生产者和消费者调用者两个方法。
public class Demo10 {
    public static void main(String[] args) {
        Person p = new Person();
        Producer pro = new Producer(p);
        Consumer con = new Consumer(p);
        Thread t1 = new Thread(pro, "生产者");
        Thread t2 = new Thread(con, "消费者");
        t1.start();
        t2.start();
    }
}

// 使用Person作为数据存储空间
class Person {
    String name;
    String gender;


    public synchronized void set(String name, String gender) {
        this.name = name;
        this.gender = gender;
    }

    public synchronized void read() {
        System.out.println("name:" + this.name + "----gender:" + this.gender);
    }

}

// 生产者
class Producer implements Runnable {
    Person p;

    public Producer() {

    }

    public Producer(Person p) {
        this.p = p;
    }

    @Override
    public void run() {
        int i = 0;
        while (true) {

            if (i % 2 == 0) {
                p.set("jack", "man");
            } else {
                p.set("小丽", "女");
            }
            i++;

        }

    }

}

// 消费者
class Consumer implements Runnable {
    Person p;

    public Consumer() {

    }

    public Consumer(Person p) {
        this.p = p;
    }

    @Override
    public void run() {

        while (true) {
            p.read();

        }
    }

}

总结:Java使用Object类的wait,notify,notifyAll这几个方法实现线程间的通信。所以在我们的需求是,生产者生产一次,消费者就消费一次的话,需要使用到线程间的通信。

传统线程通信

Lock & Condition

1.Lock

Lock功能类似传统多线程技术里的synchronized,实现线程互斥,但更加面向对象。将需要互斥的代码片段放到lock.lock();和lock.unlock();之间。

案例1:

class A{
    private Lock lock = new ReentrantLock();

    public void function(){
        lock.lock();
        try{
            //功能代码
        }finally{
            lock.unlock();
        }
    }
}
  • 读写锁 java.util.concurrent.locks:Class ReentrantReadWriteLock

案例2:javaDoc文档读写锁例子,缓存:

class CachedData {
   Object data;
   volatile boolean cacheValid;
   final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

   void processCachedData() {
     rwl.readLock().lock();
     if (!cacheValid) {
        //在这获得写锁之前必须先释放读锁
       rwl.readLock().unlock();
       rwl.writeLock().lock();
       try {
         // Recheck state because another thread might have
         // acquired write lock and changed state before we did.
         if (!cacheValid) {
           data = ...
           cacheValid = true;
         }
         // Downgrade by acquiring read lock before releasing write lock
         rwl.readLock().lock();
       } finally {
         rwl.writeLock().unlock(); // Unlock write, still hold read
       }
     }

     try {
       use(data);
     } finally {
       rwl.readLock().unlock();
     }
   }
 }

注意:在释放写锁前加读锁那部分代码,注释为// Downgrade by acquiring read lock before releasing write lock。自己挂了写锁,再挂读锁是可以的,这面涉及的技巧以后再研究。


提示:volatile表示一个类型修饰符,被设计用来修饰不同线程访问和修改的变量。被volatile类型定义的变量,系统每次用到的时候是直接从对应的内存中提取,不会利用缓存。在使用了volatile修饰成员变量后,所有线程在任何时候所看到变量的值是相同的。

注意:volatile不能保证操作的原子性,所以在一般情况下volatile不能代替sychronized。此外,使用volatile会组织编译器对代码的优化,因此会降低程序的执行效率。


5.Condition

Condition类似于传统多线程技术中的Object.wait和Object.notify,实现线程间同步。

如果程序不使用synchronized关键字来保证同步,而是直接使用Lock对象来保证同步,则系统中不存在隐式的同步监视器,不能使用 wait() | notify() | notifyAll()方法进行线程通信。

在使用Lock对象来保证同步时,Java提供一个Condition类来保持协调,使用Condition可以让那些已经得到Lock对象却无法执行的线程释放Lock对象,Condition对象也可以唤醒其它处于等待的线程。

javaDoc文档例子,可阻塞队列

class BoundedBuffer例子

class BoundedBuffer {
   final Lock lock = new ReentrantLock();
   final Condition notFull  = lock.newCondition(); 
   final Condition notEmpty = lock.newCondition(); 

   final Object[] items = new Object[100];
   int putptr, takeptr, count;

   public void put(Object x) throws InterruptedException {
     lock.lock();
     try {
       while (count == items.length)
         notFull.await();
       items[putptr] = x;
       if (++putptr == items.length) putptr = 0;
       ++count;
       notEmpty.signal();
     } finally {
       lock.unlock();
     }
   }

   public Object take() throws InterruptedException {
     lock.lock();
     try {
       while (count == 0)
         notEmpty.await();
       Object x = items[takeptr];
       if (++takeptr == items.length) takeptr = 0;
       --count;
       notFull.signal();
       return x;
     } finally {
       lock.unlock();
     }
   }
 }

提示:使用了两个condition


阻塞队列(BlockingQueue)

6.使用阻塞队列(BlockingQueue)控制线程通信

在java.util.concurrent.BlockingQueue接口有以下阻塞队列实现:

1.FIFO队列:LinkedBlockingQueue | ArrayBlockingQueue(固定长度)

2.优先级队列:PriorityBlockingQueue

该阻塞队列提供了阻塞的 take() | put()方法:如果队列为空 take() 将阻塞,直到队列中有内容。如果队列为满 put() 将阻塞,直到队列有空闲位置。

阻塞队列实现生产者和消费者的场景
package Java.ChapterSeven.BlockingQueue;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class ProducerConsumer {

    private static BlockingQueue<String> queue = new ArrayBlockingQueue<>(5);
    private static class Producer extends Thread {
        @Override
        public void run(){
            try {
                queue.put("product");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("produce..");
        }
    }

    private static class Consumer extends Thread {
        @Override
        public void run(){
            try {
                String product = queue.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("consumer..");
        }
    }

    public static void main(String[] args) {

        for (int i = 0; i < 2; i++){
            Producer producer = new Producer();
            producer.start();
        }

        for (int i = 0; i < 5; i++){
            Consumer consumer = new Consumer();
            consumer.start();
        }

        for (int i = 0; i < 3; i++){
            Producer producer = new Producer();
            producer.start();
        }
    }
}

控制线程

1.join线程

Thread提供一线程等待另一线程完成再执行本线程的方法,join()方法。当在某一个程序执行流中调用该方法,调用线程将会被阻塞,直到被join()方法加入的join线程执行完为止。

注意:在某个执行的线程中调用join()方法,调用线程将阻塞,直到被join()方法加入线程执行完。

package cn.edu.xidian.B.Demo.Thread;

public class ThreadJoinTest extends Thread{
    public ThreadJoinTest(String name) {
        super(name);
    }

    //线程执行体
    @Override
    public void run(){
        for (int i = 0; i < 10; i++){
            System.out.println("当前线程是:" + this.getName() + i);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //在主线程上再走一个子线程
        new ThreadJoinTest("newThread").start();
        for (int i = 0; i < 100; i++){
            if (i == 20){
                //在这里再次创建子线程
                ThreadJoinTest tj = new ThreadJoinTest("join线程");
                tj.start();
                //该方法不会和主线程一起执行,必须等该线程执行完之后,主线程才会继续执行
                tj.join();
            }
            System.out.println(Thread.currentThread().getName() + "" + i);
        }
    }
}

2.后台线程

后台线程特点:如果所有的前台线程死亡,后台线程会自动死亡。然后调用Thread对象的setDaemon(true)方法会将指定的线程定义为后台线程。同时这里定义后台线程与普通线程没有区别。

package cn.edu.xidian.B.Demo.Thread;

public class DaemonThread extends Thread {
    public DaemonThread(String name) {
        super(name);
    }

    @Override
    public void run(){
        for (int i = 0; i < 10; i++){
            System.out.println("当前线程是:" + this.getName() + i);
        }
    }

    public static void main(String[] args) {
        DaemonThread t = new DaemonThread("DaemonThread");
        //设置后台线程,此方法需要在start()方法之前调用
        t.setDaemon(true);
        //启动后台线程
        t.start();
        for (int i = 0; i < 10; i++){
            System.out.println(Thread.currentThread().getName() + "" + i);
        }
        //在主线程执行完之后,后台线程也会自动执行结束
    }
}

3.线程让步 yield()方法

该方法只是Thread类提供的一个静态方法,它可以让当前正在执行的线程暂停,但不会阻塞该线程,只是将该线程转入就绪状态。

实际上,当某个线程调用yield()方法之后,只有优先级与当前线程相同或者优先级比当前线程更高的处于就绪状态的线程才会获得执行机会。

4.线程睡眠 sleep()方法

5.同步代码块 synchronized()方法


线程相关类

1.ThreadLocal类

1.T get() 方法
2.void remove() 方法
3.void set(T value) 方法

中断

一个线程执行完毕之后会自动结束,如果在运行过程中发生异常也会提前结束。

InterruptedException

通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。

interrupted()

如果一个线程的run()方法执行一个无限循环,并且没有执行sleep()等将会抛出 InterruptedException异常,在调用 interrupt() 方法就无限使得线程提前结束。

但是调用 interrupt() 方法会设置线程的中断标记,此时调用 interrupted() 方法会返回 true。因此可以在循环体中使用 interrupted() 方法来判断线程是否处于中断状态,从而提前结束线程。

Executor 的中断操作

调用 Executor 的 shutdown() 方法会等待线程都执行完毕之后再关闭,但是如果调用的是 shutdownNow() 方法,则相当于调用每个线程的 interrupt() 方法。

这里使用lambda创建线程,相当于创建一个匿名内部线程:

package Java.ChapterSeven.Executor;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class ExecutorSecondOne {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
//        executorService.execute(() -> {
//            try {
//                Thread.sleep(2000);
//                System.out.println("thread run");
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }
//        });
//        executorService.shutdownNow();


        Future<?> future = executorService.submit(() -> {
            try {
                Thread.sleep(2000);
                System.out.println("thread run");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        future.cancel(true);

        System.out.println("main run");
    }
}

提示:通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。因为在第一步的时候,在子线程内部调用了sleep()方法使得该线程处于限期等待状态。

提示:在第二步的过程中通过使用 submit() 方法来提交一个线程,它会返回一个 Future<?> 对象,通过调用该对象的 cancel(true) 方法就可以中断线程。

互斥同步

在Java中提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock。

1.同步代码块

使用 ExecutorService 执行了两个线程,由于调用的是同一个对象的同步代码块,因此这两个线程会进行同步,当一个线程进入同步语句块时,另一个线程就必须等待。

package Java.ChapterSeven.SynchronizedDemo;

public class SynchronizedDemo {

    public void funOne(){
        synchronized (this){
            for (int i = 0; i < 10; i++){
                System.out.println(i + " ");
            }
        }
    }
}

package Java.ChapterSeven.SynchronizedDemo;

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

public class Run {
    public static void main(String[] args) {
        SynchronizedDemo sn = new SynchronizedDemo();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(() -> sn.funOne());
        executorService.execute(() -> sn.funOne());
    }
}

两个线程调用了不同对象的同步代码块,因此这两个线程就不需要同步。从输出结果可以看出,两个线程交叉执行。

package Java.ChapterSeven.SynchronizedDemo;

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

public class Run {
    public static void main(String[] args) {
        SynchronizedDemo snOne = new SynchronizedDemo();
        SynchronizedDemo snTwo = new SynchronizedDemo();

        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(() -> snOne.funOne());
        executorService.execute(() -> snTwo.funOne());
    }
}

2.同步方法

3.同步一个类

作用与整个类,两个线程调用同一个类的不同对象的同步语句,也会进行同步。

package Java.ChapterSeven.SynchronizedDemo;

public class SynchronizedDemoTwo {

    public void funTwo(){
        //类同步
        synchronized (SynchronizedDemoTwo.class){
            for (int i = 0; i < 10; i++){
                System.out.println(i + " ");
            }
        }
    }
}

package Java.ChapterSeven.SynchronizedDemo;

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

public class Run {
    public static void main(String[] args) {
        SynchronizedDemoTwo snOne = new SynchronizedDemoTwo();
        SynchronizedDemoTwo snTwo = new SynchronizedDemoTwo();

        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(() -> snOne.funTwo());
        executorService.execute(() -> snTwo.funTwo());
    }
}

4.同步静态方法

作用于整个类。


ReentrantLock

ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁。

package Java.ChapterSeven.ReentrantLock;

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

public class ReentrantLockDemo {

    private Lock lock = new ReentrantLock();
    public void funOne(){
        //实现同步
        lock.lock();
        try {
            for (int i = 0; i < 10; i++){
                System.out.println(i + " ");
            }
        }finally {
            lock.unlock();
        }
    }
}

package Java.ChapterSeven.ReentrantLock;

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

public class Run {
    public static void main(String[] args) {
        ReentrantLockDemo rt = new ReentrantLockDemo();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(() -> rt.funOne());
        executorService.execute(() -> rt.funOne());
    }
}

synchronized | ReentrantLock 比较

1.synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。

2.新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。

3.当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。ReentrantLock 可中断,而 synchronized 不行。

4.公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。

5.一个 ReentrantLock 可以同时绑定多个 Condition 对象。

J.U.C - AQS

java.util.concurrent(J.U.C)大大提高了并发性能,AQS 被认为是 J.U.C 的核心。

1.CountDownLatch

其用来控制一个线程等待多个线程。维护了一个计数器 cnt,每次调用 countDown() 方法会让计数器的值减 1,减到 0 的时候,那些因为调用 await() 方法而在等待的线程就会被唤醒。

package Java.ChapterSeven.CountdownLatch;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CountdownLatchDemo {

    public static void main(String[] args) throws InterruptedException {
        final int totalThread = 10;
        CountDownLatch countdownLatch = new CountDownLatch(totalThread);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < totalThread; i++){
            executorService.execute(() -> {
                System.out.print("run ..");
                countdownLatch.countDown();
            });
        }
        countdownLatch.await();
        System.out.println("end");
        executorService.shutdown();
    }
}

2.CyclicBarrier

控制多个线程互相等待,只有多个线程都到达时,这些线程才会继续执行。

CyclicBarrier 和 CountdownLatch 相似,都是通过维护计数器来实现的。线程执行 await() 方法之后计数器会减 1,并进行等待,直到计数器为 0,所有调用 await() 方法而在等待的线程才能继续执行。

提示:CyclicBarrier 和 CountdownLatch 的一个区别是,CyclicBarrier 的计数器通过调用 reset() 方法可以循环使用,所以它才叫做循环屏障。

CyclicBarrier 有两个构造函数,其中 parties 指示计数器的初始值,barrierAction 在所有线程都到达屏障的时候会执行一次。

public CyclicBarrier(int parties, Runnable barrierAction) {
    if (parties <= 0) throw new IllegalArgumentException();
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
}

public CyclicBarrier(int parties) {
    this(parties, null);
}


package Java.ChapterSeven.CyclicBarrier;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class CyclicBarrierExample {

    public static void main(String[] args) {
        final int totalThread = 10;
        CyclicBarrier cyclicBarrier = new CyclicBarrier(totalThread);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < totalThread; i++){
            executorService.execute(() -> {
                System.out.println("before..");
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
                System.out.println("after..");
            });
        }
        executorService.shutdown();
    }
}

3.Semaphore

Semaphore 类似于操作系统中的信号量,可以控制对互斥资源的访问线程数。

这里程序模拟对某个服务的并发请求,每次只能有3个客户端同时访问,请求总数是10。

package Java.ChapterSeven.Semaphore;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;

public class SemaphoreExample {
    public static void main(String[] args) {
        final int clientCount = 3;
        final int totalRequestCount = 10;
        Semaphore semaphore = new Semaphore(clientCount);
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < totalRequestCount; i++){
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    System.out.println(semaphore.availablePermits() + " ");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    semaphore.release();
                }
            });
        }
        executorService.shutdown();
    }
}

未完…

本文标题:Java 多线程

文章作者:Bangjin-Hu

发布时间:2019年06月06日 - 09:22:26

最后更新:2020年03月30日 - 08:11:08

原始链接:http://bangjinhu.github.io/undefined/Java 多线程/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

Bangjin-Hu wechat
欢迎扫码关注微信公众号,订阅我的微信公众号.
坚持原创技术分享,您的支持是我创作的动力.