Java 多线程 详解

前言:

并发和并行

并发指的是在同一时刻只能有一个进程运行,但多个进程可以来回迅速切换,使得在宏观上具有多个进程同时运行的效果,而并行是同一时刻有多条指令在多个处理器上同时执行。

线程和进程

线程也叫轻量级进程,线程是进程的组成部分,一个进程可以拥有多个线程,一个线程必须有一个父进程,线程共享父进程的所有资源
线程是独立运行的,不知道有其他线程运行,执行是抢占式的。
线程的调度和管理由进程本身完成

多线程编程的优点:

  1. 进程之间不能共享内存,但线程之间共享内存很容易
  2. 系统创建进程时要为进程重新分配资源,但创建线程要容易的多,代价小得多,因此使用多线程来实现任务并发要比多进程效率高
  3. Java内置了多线程功能支持,简化了编程

什么是线程安全

当多个线程访问某个方法时,不管你通过怎样的调用方式、或者说这些线程如何交替地执行,我们在主程序中不需要去做任何的同步,这个类的结果行为都是我们设想的正确行为,那么我们就可以说这个类是线程安全的。

线程的创建和启动

继承Thread的线程创建:

一、 继承并定义Thread的子类,重写该类的run()方法,run就是该线程要执行的内容
二、创建子类的实例方法
三、调用线程对象的start()方法来启动线程

package com.company;

public class FirstThread extends Thread {
    private int i ;

    @Override
    public void run() {

        for(;i<100;i++){
            System.out.println(getName()+" "+ i);
        }
    }

    public static void main(String[] args) {

        for(int i = 0;i < 100;i ++){
            System.out.println(Thread.currentThread().getName()+" "+i);
            if(i == 20){

               new FirstThread().start();
               new FirstThread().start();


            }
        }
    }
}

main 24
Thread-1 1
Thread-0 1
Thread-1 2
main 25
Thread-1 3
Thread-0 2
Thread-1 4
main 26
Thread-1 5
Thread-0 3
Thread-1 6
main 27

线程运行时除了用户自定义的线程还有main的默认主线程
可以看到两个线程的值是不连续的各走各的,因为使用继承Thread类的方法来创建线程时,多个线程之间无法共享线程类的实例变量。

实现Runnable接口来创建线程类

一、定义Runnable接口的实现类,重写该接口的run()方法,该方法是该线程的方法执行体
二、创建接口实现类的实例,并以此实例作为Thread的target来创建Thread对象(真正的线程对象)利用有参构造器传入,也可在构造器中传入该线程的自定义名字

package com.company;

public class FirstThread implements Runnable {
    private int i ;

    @Override
    public void run() {

        for(;i<100;i++){
            System.out.println(Thread.currentThread().getName()+" "+ i);
        }
    }

    public static void main(String[] args) {

        for(int i = 0;i < 100;i ++){
            System.out.println(Thread.currentThread().getName()+" "+i);
            if(i == 20){
                FirstThread fs =new FirstThread();
               new Thread(fs,"the first thread").start();
               new Thread(fs,"the second thread").start();


            }
        }
    }
}

C:\java\jdk\bin\java.exe "-javaagent:E:\IntelliJ IDEA 2019.2\lib\idea_rt.jar=63578:E:\IntelliJ IDEA 2019.2\bin" -Dfile.encoding=GBK -classpath C:\java\jdk\jre\lib\charsets.jar;C:\java\jdk\jre\lib\deploy.jar;C:\java\jdk\jre\lib\ext\access-bridge-64.jar;C:\java\jdk\jre\lib\ext\cldrdata.jar;C:\java\jdk\jre\lib\ext\dnsns.jar;C:\java\jdk\jre\lib\ext\jaccess.jar;C:\java\jdk\jre\lib\ext\jfxrt.jar;C:\java\jdk\jre\lib\ext\localedata.jar;C:\java\jdk\jre\lib\ext\nashorn.jar;C:\java\jdk\jre\lib\ext\sunec.jar;C:\java\jdk\jre\lib\ext\sunjce_provider.jar;C:\java\jdk\jre\lib\ext\sunmscapi.jar;C:\java\jdk\jre\lib\ext\sunpkcs11.jar;C:\java\jdk\jre\lib\ext\zipfs.jar;C:\java\jdk\jre\lib\javaws.jar;C:\java\jdk\jre\lib\jce.jar;C:\java\jdk\jre\lib\jfr.jar;C:\java\jdk\jre\lib\jfxswt.jar;C:\java\jdk\jre\lib\jsse.jar;C:\java\jdk\jre\lib\management-agent.jar;C:\java\jdk\jre\lib\plugin.jar;C:\java\jdk\jre\lib\resources.jar;C:\java\jdk\jre\lib\rt.jar;H:\ideaproject\temp1\out\production\temp1;H:\ideaproject\temp1\lib\HdfsBrowser-20171108.jar com.company.FirstThread
main 0
main 1
main 2
main 3
main 4
main 5
main 6
main 7
main 8
main 9
main 10
main 11
main 12
main 13
main 14
main 15
main 16
main 17
main 18
main 19
main 20
main 21
main 22
main 23
main 24
the second thread 0
the first thread 0
main 25
the first thread 2
the second thread 1
the second thread 4
the second thread 5
the second thread 6
the second thread 7
the second thread 8
the second thread 9
the first thread 3
main 26
the first thread 11
the second thread 10
the second thread 13
the first thread 12
main 27
main 28
main 29
main 30
main 31
main 32
the first thread 15
.......

由运行结果可以发现两个线程由于是公用一个实例化对象fs,所以值是连续的,不是各走各的,说明采用Runnable接口的方式创建的多个线程可以共享线程类的实例属性,因为多个线程共享同一个target。

使用Callable和Future创建线程

从Java5开始,提供了Callable接口,是Runnable接口的升级版,他的call()方法比 Runnable的 run()方法强大的多
callable

@see Executor
 * @since 1.5
 * @author Doug Lea
 * @param <V> the result type of method {@code call}
 */
@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

可以看出
call 方法可以有返回值。
call 方法可以抛出异常。

Runnable

* @see     java.lang.Thread
 * @see     java.util.concurrent.Callable
 * @since   JDK1.0
 */
@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

创建过程:

一、实现Callable接口,实现call()方法(线程执行体),该call()方法有返回值。
二、创建 Callable实现类的实例,并拿FutureTask包装
三、将Futuretask对象作为Thread的target创建并启用线程
四、调用FutureTask对象的gat()获取 线程结束后的返回值。

注意:

运行Callable任务可以拿到一个Future对象,Future 表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。计算完成后只能使用 get 方法来获取结果,如果线程没有执行完,Future.get()方法可能会阻塞当前线程的执行;如果线程出现异常,Future.get()会throws InterruptedException或者ExecutionException;如果线程已经取消,会跑出CancellationException。取消由cancel 方法来执行。isDone确定任务是正常完成还是被取消了。一旦计算完成,就不能再取消计算。如果为了可取消性而使用 Future 但又不提供可用的结果,则可以声明Future<?> 形式类型、并返回 null 作为底层任务的结果。Future接口的定义如下:

Future模式
Future模式在请求发生时,会先产生一个Future凭证给发出请求的客户,它的作用就像是Proxy物件,
同时,由一个新的执行线程持续进行目标物件的生成(Thread-Per-Message),真正的目标物件生
成之后,将之设定至Future之中,而当客户端真正需要目标物件时,目标物件也已经准备好,可以让
客户提取使用。结合JDK的Future来看,就是你run线程后,你可以把线程的返回值赋给Future并返回
一个Future对象。这时你可以立	即拿到这个对象,然后进行下面的逻辑。但是如果你要get这个Future
中的线程结果,就会被阻塞直到线程结束。就相当于现在的期房,你把手续和钱都交上去了,就可以
马上拿到合同,但只有合同没有房子。这个时候你已经是有房	一族了,你可以先去买家电买装修
(走下面的其他逻辑)。但是你要把家电和装修放进去,就必须等到房子完工(阻塞)。



		[这里引用了:]https://blog.csdn.net/HEYUTAO007/article/details/19072675

说白了就是Callable的实例对象必须要有一个返回值,而这个返回值不能马上得到,要等线程方法call()跑完了才能得到,不能直接拿Thread的target去盛,这里就用Future去转接一下,Future就是一个类似于花呗系统的东西会get出来你未来要还的东西,然后先把钱给你花。。。

package com.company;

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

public class SecondThread implements Callable<Integer> {
    int i;
    @Override
    public Integer call() throws Exception {
        for(;i < 100;i++){
            System.out.println(Thread.currentThread().getName()+i);
        }
        return i;
    }

    public static void main(String[] args) {
        SecondThread sc =new SecondThread();
        FutureTask<Integer> future = new FutureTask<Integer>(sc);
        for(int i = 0;i < 100;i ++){
            System.out.println(Thread.currentThread().getName()+i);
            if(i == 20){
                new Thread(future,"带返回值的方法").start();

                try {
                    System.out.println("子线程的返回值"+future.get());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

比较三种线程创建方法:

Runnable 和 Callable都是接口实现,可以继承其他类
这两种方式下 多个线程可以共享一个target对象
Callable可以返回线程运行后的结果和自己抛出异常,Runnable只能内部消化

线程的生命周期

线程一旦启动,不会立马进入执行状态,也不是一直处于执行状态。

线程的五种状态

新建 
就绪
运行
阻塞
死亡

新建和运行线程

new 一个Thread 创建线程,线程就新建好了,如果让线程运行则需要启用start()方法

start和run方法的不同:

run只是一个线程执行的函数,线程的启动则应该用start而不是run,如果用了run,则只不过是运行线程的执行体,单线程执行不会并发的执行,只是简单的顺序的程序。
注意: 只能对处于新建状态的线程调用start()方法

运行和阻塞状态

阻塞:

  1. 线程调用了sleep()方法主动放弃了所占用的处理器资源
  2. 线程调用了一个阻塞式IO方法,在该方法返回前,该线程被阻塞

死亡

isAlive()判断线程是否存在,
新建和死亡时返回false
阻塞和运行时返回true

线程的控制

join方法

join是一个让线程等线程的方法,当某个线程执行流中调用其他线程的join()方法,该线程会被阻塞,直到被join的方法的join线程执行完毕。

public class FirstThread extends Thread {
    class SecondThread extends Thread{
        @Override
        public void run() {
            for(int i = 0;i<200;i++){
                System.out.println("我是第二个join");
            }
        }
    }
    private int i;
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"启动了");
        for(int i=0;i<100;i++){
            System.out.println(getName()+"他的i = "+i);
            if(i==20){

                try {
                    SecondThread fs2 = new SecondThread();
                    fs2.start();
                    fs2.join();

                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

Thread-0他的i = 19
Thread-0他的i = 20
我是第二个join
我是第二个join
我是第二个join
我是第二个join
我是第二个join
我是第二个join
我是第二个join
我是第二个join
我是第二个join
我是第二个join
Thread-0他的i = 21

后台线程

将一个线程设为后台线程后他会成为守护线程,直到所有线程都结束,他才能结束
调用Thread的setDaemon(true)方法可将指定线程设为后台线程

Sleep

让当前的线程暂停一会并进入阻塞状态,可以通过调用Thread的静态sleep()来实现。
sleep的方法有两种重载形式。
没啥用,关于时间精度问题。

线程让步 :yield

yield:v.生产;获利;屈服;弯下去

n.投资收益;生产量
他是Thread类提供的一个静态方法,通过它,可以暂停线程,让线程由运行状态转入到就绪状态,并不会使该线程阻塞。
yield()只是让系统的线程调度器对该线程重新调度了一下,
如果a和b和其他线程一起运行,在某个时刻,a调用了yield()a由运行转为了就绪,接下来a可能继续霸占资源立马又继续运行,也可能把运行的资源让给比他高优先级的线程运行,注意这里说的都是可能,
举个例子:一帮朋友在排队上公交车,轮到Yield的时候,他突然说:我不想先上去了,咱们大家来竞赛上公交车。然后所有人就一块冲向公交车,

有可能是其他人先上车了,也有可能是Yield先上车了。

但是线程是有优先级的,优先级越高的人,就一定能第一个上车吗?这是不一定的,优先级高的人仅仅只是第一个上车的概率大了一点而已,

最终第一个上车的,也有可能是优先级最低的人。并且所谓的优先级执行,是在大量执行次数中才能体现出来的。

sleep 和yield 的区别

  1. sleep 暂停当前线程后会直接让给别的线程不会理会其他线程的优先级,但是yield方法只会给优先级和自己相同或者优先级高于自己的线程
  2. sleep()方法暂停线程后会进入阻塞,经过一段时间的阻塞才会转入就绪状态,而yield()方法直接强制线程进入就绪状态,很有可能,一个调用yield的线程经过短暂的暂停后直接又被处理器执行
  3. sleep()方法抛出InrerruptedException异常,而yield方法没有抛出任何异常的能力
  4. sleep()比yield()有更好的移植性,所以一般情况下使用sleep();

改变线程的优先级

高优先级的线程会被高概率执行,低优先级的线程会被低概率执行
父线程的优先级和他的子线程的优先级是一样的,
Thread类提供了setPriority 和getPriority Priority的优先级有十级,但是不同的操作系统有不同的优先级,所以用下面三个常量来设置优先级
对优先级进行操作,
MAX_PRIORITY: 10
MIN_PRIORITY: 1
NORM_PRIORITY: 5

线程同步

线程安全问题

银行取钱问题:
如果不考虑线程安全问题,实现一个取钱的业务逻辑并且运行多次:
建立用户类:建立用户id号,和余额字段

package com.company.bank;

import java.util.Objects;

public class Account {
    public int getAccountNo() {
        return accountNo;
    }

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

    public int getBalance() {
        return balance;
    }

    public void setBalance(int balance) {
        this.balance = balance;
    }

    private int accountNo;
    private int balance;

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


    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Account account = (Account) o;
        return accountNo == account.accountNo &&
                balance == account.balance;
    }

    @Override
    public int hashCode() {
        return Objects.hash(accountNo, balance);
    }
}

建立取钱业务:

package com.company.bank;

public class DrawThread extends Thread{
    private String name;
    private int drawAccount;
    private Account account;
    public DrawThread(String name,int drawAccount,Account account){
        this.name = name;
        this.account = account;
        this.drawAccount = drawAccount;
    }

    @Override
    public void run() {
        super.run();
            if (drawAccount <= account.getBalance()) {
                account.setBalance(account.getBalance() - drawAccount);
                System.out.println(name + "取钱成功---账户余额=" + account.getBalance());
            } else {
                System.out.println(name + "取钱失败,余额不足");
            }

    }
}

运行:

public class Test {
    public static void main(String[] args) {
        Account account = new Account(1,1000);
        new DrawThread("A",800,account).start();
        new DrawThread("B",100,account).start();
        //System.out.println("余额为"+account.getBalance());

    }
}

失败案例:

B取钱成功---账户余额=200
A取钱成功---账户余额=200

Process finished with exit code 0

很明显是错的。

同步代码块

为了解决两个并发的线程对同一对象资源进行改写所带来的的错误,java多线程支持引入同步监视器来解决这种问题。
关于sychornized关键字的详解,参见synchronized底层如何实现及应用

sychronized(account){
		...
}

同步方法

用 sychronized关键字修饰某个方法,该方法称为同步方法,无须指定同步监视器,监视器就是他自己this的对象本身。

线程安全的类有以下特征:

  • 该类的对像可被多个线程安全的访问
  • 每个线程调用该对象的任意方法都会得到正确的结果
  • 每个线程调用该类的方法后,该对象状态依然保持合理的状态

不可变的类总是线程安全的,而可变的类会有线程不安全

可变类的线程安全是以牺牲程序运行效率为代价的,为了减少程序线程安全所带来的负面影响,可以采用以下措施

  • 不要对线程安全类的所有方法都进行同步,只对改变竞争资源的方法进行同步,
  • 如果可变类有两种运行环境:单线程和多线程,应该为可变类提供两种版本,线程呢个安全版本和不安全版本,让不安全版本在单线程环境中保持良好的性能,例如:StringBuilder,StringBuffer单线程下应该用StringBuilder来保证性能,多线程下用StringBuffer保持同步和线程安全

释放同步监视器的锁定

程序无法显式地释放对同步监视器的锁定,在以下情况下会自动释放

  • 当前同步代码块运行结束
  • 线程在同步代码块同步方法中遇到break,return 终止了代码块,方法的执行
  • 程序执行了同步监视器的wait()方法,当前线程暂停

以下情况情况,线程不会释放同步监视器

  • 调用sleep(),yield()
  • suspend()挂起线程

同步锁

临界区

当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竞态条件发生的代码区称作临界区。在临界区中使用适当的同步就可以避免竞态条件。
界区实现方法有两种,一种是用synchronized,一种是用Lock显式锁实现。

有临界区是为了让更多的其它线程能安全够访问资源。
说白了:临界区就是修改对象状态标记的代码区

一、Lock和synchronized有以下几点不同:

1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现,synchronized是在JVM层面上实现的,不但可以通过一些监控工具监控synchronized的锁定,而且在代码执行时出现异常,JVM会自动释放锁定,但是使用Lock则不行,lock是通过代码实现的,要保证锁定一定会被释放,就必须将 unLock()放到finally{} 中;

2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;

3)Lock可以让等待锁的线程响应中断,线程可以中断去干别的事务,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;

4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。

5)Lock可以提高多个线程进行读操作的效率。

在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

举个例子:当有多个线程读写文件时,读操作和写操作会发生冲突现象,写操作和写操作会发生冲突现象,但是读操作和读操作不会发生冲突现象。

但是采用synchronized关键字来实现同步的话,就会导致一个问题:

如果多个线程都只是进行读操作,所以当一个线程在进行读操作时,其他线程只能等待无法进行读操作。

因此就需要一种机制来使得多个线程都只是进行读操作时,线程之间不会发生冲突,通过Lock就可以办到。

另外,通过Lock可以知道线程有没有成功获取到锁。这个是synchronized无法办到的

二、ReentrantLock获取锁定与三种方式:
  a) lock(), 如果获取了锁立即返回,如果别的线程持有锁,当前线程则一直处于休眠状态,直到获取锁
  b) tryLock(), 如果获取了锁立即返回true,如果别的线程正持有锁,立即返回false;
  c)tryLock(long timeout,TimeUnit unit), 如果获取了锁定立即返回true,如果别的线程正持有锁,会等待参数给定的时间,在等待的过程中,如果获取了锁定,就返回true,如果等待超时,返回false;
  d) lockInterruptibly:如果获取了锁定立即返回,如果没有获取锁定,当前线程处于休眠状态,直到或者锁定,或者当前线程被别的线程中断

package com.company.bank2;

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

public class Account {
    private final Lock lock = new ReentrantLock();
    public int getAccountNo() {
        return accountNo;
    }

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

    public int getBalance() {
        return balance;
    }

    public void setBalance(int balance) {
        this.balance = balance;
    }

    private int accountNo;
    private int balance;

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


    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Account account = (Account) o;
        return accountNo == account.accountNo &&
                balance == account.balance;
    }

    @Override
    public int hashCode() {
        return Objects.hash(accountNo, balance);
    }
    public void draw (int drawAccount){
        lock.lock();

        if (drawAccount <= this.getBalance()) {
            this.setBalance(this.getBalance() - drawAccount);
            System.out.println(Thread.currentThread().getName() + "取钱成功---账户余额=" + this.getBalance());
        } else {
            System.out.println(Thread.currentThread().getName() + "取钱失败,余额不足");
        }
        lock.unlock();
    }

}

package com.company.bank2;

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

public class DrawThread extends Thread{
    Account account;
    int Balance;

    public DrawThread(Account account, int balance) {
        this.account = account;
        Balance = balance;
    }
    void run1(){
        account.draw(this.Balance);
    }
}

package com.company.bank2;

public class Test {
    public static void main(String[] args) {
        Account account = new Account(1,1000);
        new Thread(){
            @Override
            public void run() {
                new DrawThread(account,800).run1();
            }

        }.start();
        new Thread(){
            @Override
            public void run() {
                new DrawThread(account,800).run1();
            }
        }.start();

        //System.out.println("余额为"+account.getBalance());

    }
}

死锁

创建两个字符串a和b,再创建两个线程A和B,让每个线程都用synchronized锁住字符串(A先锁a,再去锁b;B先锁b,再锁a),如果A锁住a,B锁住b,A就没办法锁住b,B也没办法锁住a,这时就陷入了死锁。

package com.company.bank;


public class DeadLock {
        public static String obj1 = "obj1";
        public static String obj2 = "obj2";
        public static void main(String[] args){
            Thread a = new Thread(new Lock1());
            Thread b = new Thread(new Lock2());
            a.start();
            b.start();
        }
    }
    class Lock1 implements Runnable{
        @Override
        public void run(){
            try{
                System.out.println("Lock1 running");
                while(true){
                    synchronized(DeadLock.obj1){
                        System.out.println("Lock1 lock obj1");
                        Thread.sleep(3000);//获取obj1后先等一会儿,让Lock2有足够的时间锁住obj2
                        synchronized(DeadLock.obj2){
                            System.out.println("Lock1 lock obj2");
                        }
                    }
                }
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }
    class Lock2 implements Runnable{
        @Override
        public void run(){
            try{
                System.out.println("Lock2 running");
                while(true){
                    synchronized(DeadLock.obj2){
                        System.out.println("Lock2 lock obj2");
                        Thread.sleep(3000);
                        synchronized(DeadLock.obj1){
                            System.out.println("Lock2 lock obj1");
                        }
                    }
                }
            }catch(Exception e){
                e.printStackTrace();
            }
        }
    }

Lock1 running
Lock1 lock obj1
Lock2 running
Lock2 lock obj2

Process finished with exit code -1

线程通信

一、用while轮序的手段,比较低端,效率不高:

package com.company.bank;

import java.util.ArrayList;
import java.util.List;

public class MyList {
    private volatile List<String> list = new ArrayList<String>();
    public void add(){
        list.add("abc");
    }
    public int size(){
        return list.size();
    }
    static class ThreadA implements Runnable{
        private MyList list;
        public ThreadA(MyList list){
            this.list = list;

        }
        @Override
        public void run() {
            for(int i = 0;i < 3;i ++) {
                System.out.println(Thread.currentThread().getName() + "正在执行。。");
                list.add();
                System.out.println("添加了" + (i + 1) + "个元素");

                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    static class ThreadB implements Runnable{
        private MyList list ;

        public ThreadB(MyList list) {
            this.list = list;
        }

        @Override
        public void run() {
            try {
                while (true) {
                    System.out.println(Thread.currentThread().getName() + "正在执行。。");
                    if (list.size() == 2) {
                        System.out.println("线程B要退出了?");

                        throw new InterruptedException();

                        //throw new InterruptedException();
                    }
                }

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        MyList list = new MyList();
        ThreadA a = new ThreadA(list);
        ThreadB b = new ThreadB(list);
        new Thread(a).start();
        new Thread(b).start();
    }

}

结果:

Thread-1正在执行。。
Thread-1正在执行。。
Thread-1正在执行。。
Thread-0正在执行。。
添加了2个元素
Thread-1正在执行。。
线程B要退出了?
java.lang.InterruptedException
	at com.company.bank.MyList$ThreadB.run(MyList.java:50)
	at java.lang.Thread.run(Thread.java:745)
Thread-0正在执行。。
添加了3个元素

二、用wait\notify机制

wait/notify 机制
在这之前,线程间通过共享数据来实现通信,即多个线程主动地读取一个共享数据,通过 同步互斥访问机制保证线程的安全性。等待/通知机制主要由Object类中的wait()、notify() 和 notifyAll()三个方法来实现,这三个方法均非Thread类中所声明的方法,而是Object类中声明的方法。原因是每个对象都拥有monitor(锁),所以让当前线程等待某个对象的锁,当然应该通过这个对象来操作,而不是用当前线程来操作,因为当前线程可能会等待多个线程的锁,如果通过线程来操作,就非常复杂了。

1.wait()——让当前线程 (Thread.concurrentThread() 方法所返回的线程) 释放对象锁并进入等待(阻塞)状态。
2.notify()——唤醒一个正在等待相应对象锁的线程,使其进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。
3.notifyAll()——唤醒所有正在等待相应对象锁的线程,使它们进入就绪队列,以便在当前线程释放锁后竞争锁,进而得到CPU的执行。
从以上描述可以得出:wait()、notify() 和 notifyAll()方法是 本地方法,并且为 final 方法,无法被重写;调用某个对象的 wait() 方法能让 当前线程阻塞,并且当前线程必须拥有此对象的monitor(即锁);调用某个对象的 notify() 方法能够唤醒 一个正在等待这个对象的monitor的线程,如果有多个线程都在等待这个对象的monitor,则只能唤醒其中一个线程;调用notifyAll()方法能够唤醒所有正在等待这个对象的monitor的线程。

package com.company;

public class Comunicate {
    static Object object = new Object();

    public static void main(String[] args) {
        ThreadA  a = new ThreadA();
        ThreadB b = new ThreadB();
        a.start();
        b.start();
        //System.out.println("是否停止1?="+a.interrupted());
        //if(Thread.currentThread().isInterrupted()) System.out.println(Thread.currentThread().getName()+"阻塞了");

    }

    static class ThreadA extends Thread {

        public Boolean isInterrupt(){
            return this.isInterrupted();
        }
        public void run() {
            synchronized (object) {
                System.out.println(Thread.currentThread().getName() + "获得了锁");

                try {
                    System.out.println(Thread.currentThread().getName() + "释放该锁进入阻塞");
                    
                    object.wait();


                } catch (InterruptedException e) {
                   e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "线程结束");


            }


        }


    }
    static class ThreadB extends Thread{
        public void run(){
            synchronized (object){
                System.out.println(Thread.currentThread().getName()+"该线程获得了锁");
                object.notify();
                System.out.println(Thread.currentThread().getName()+"该线程唤醒了正在等待的线程");
                System.out.println("线程执行完毕");
            }
        }
    }
}

Thread-0获得了锁
Thread-0释放该锁进入阻塞
Thread-1该线程获得了锁
Thread-1该线程唤醒了正在等待的线程
线程执行完毕
Thread-0线程结束

1、每个对象都有一个锁来控制同步访问,Synchronized关键字可以和对象的锁交互,来实现同步方法或同步块。sleep()方法正在执行的线程主动让出CPU(然后CPU就可以去执行其他任务),在sleep指定时间后CPU再回到该线程继续往下执行(注意:sleep方法只让出了CPU,而并不会释放同步资源锁!!!);wait()方法则是指当前线程让自己暂时退让出同步资源锁,以便其他正在等待该资源的线程得到该资源进而运行,只有调用了notify()方法,之前调用wait()的线程才会解除wait状态,可以去参与竞争同步资源锁,进而得到执行。(注意:notify的作用相当于叫醒睡着的人,而并不会给他分配任务,就是说notify只是让之前调用wait的线程有权利重新参与线程的调度);

2、sleep()方法可以在任何地方使用;wait()方法则只能在同步方法或同步块中使用;

3、sleep()是线程线程类(Thread)的方法,调用会暂停此线程指定的时间,但监控依然保持,不会释放对象锁,到时间自动恢复;wait()是Object的方法,调用会放弃对象锁,进入等待队列,待调用notify()/notifyAll()唤醒指定的线程或者所有线程,才会进入锁池,不再次获得对象锁才会进入运行状态;

三、Condition

Condition是在java 1.5中才出现的,它用来替代传统的Object的wait()、notify()实现线程间的协作,相比使用Object的wait()、notify(),使用Condition的await()、signal()这种方式实现线程间协作更加安全和高效。因此通常来说比较推荐使用Condition,阻塞队列实际上是使用了Condition来模拟线程间协作。

Condition是个接口,基本的方法就是await()和signal()方法;
Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition()

调用Condition的await()和signal()方法,都必须在lock保护之内,就是说必须在lock.lock()和lock.unlock之间才可以使用
Conditon中的await()对应Object的wait();

Condition中的signal()对应Object的notify();

Condition中的signalAll()对应Object的notifyAll()。
在这里插入图片描述

实现一个简单的消费者生产者模型:

package com.company.bank;

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

public class Conditon1 {
    static final Lock lock = new ReentrantLock();
    static final Condition con = lock.newCondition();

    public static void main(String[] args) {
        new Consumer().start();
        new Producer().start();
    }

    static class Consumer  extends Thread{
        @Override
        public void run() {
            super.run();
            consume();
        }

        private void consume() {
            lock.lock();
            System.out.println(Thread.currentThread().getName()+"我在等一个新信号");
            try {
                con.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                System.out.println(Thread.currentThread().getName()+"拿到一个新信号");
                System.out.println(Thread.currentThread().getName()+"完事了");
                lock.unlock();
            }


        }
    }
    static class Producer extends Thread{
        @Override
        public void run() {
            super.run();
            produce();
        }

        private void produce() {
            try{lock.lock();
            System.out.println(Thread.currentThread().getName()+"我拿到一个锁");
            con.signalAll();
            System.out.println("我发出一个锁");
        }finally {
                System.out.println(Thread.currentThread().getName()+"完事了");
                lock.unlock();
            }
        }
    }
}


Thread-0我在等一个新信号
Thread-1我拿到一个锁
我发出一个锁
Thread-1完事了
Thread-0拿到一个新信号
Thread-0完事了

Process finished with exit code 0

这里的原理其实就是consumer 拿到一个锁,然后await交出锁,阻塞,给了producer

producer拿到锁,进行一些操作,然后再还回来,producer线程结束,还回来的锁又给了阻塞的consumer,consumer拿到锁然后结束,

锁就是一个菜单,消费者拿到然后给厨师,厨师拿到菜单做饭,然后把需要做的菜做好再还会给消费者,整个过程结束

实现一个标准的消费者生产者模型

package com.company.bank;

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

public class Condition2 {
    private static final Lock lock = new ReentrantLock();
    private static final Condition un_empty = lock.newCondition();
    private static final Condition un_full = lock.newCondition();
    static private Queue<Integer> queue = new LinkedList<Integer>();

    public static void main(String[] args) {
        new Consumer().start();
        new Produce().start();
    }

    static class Consumer extends Thread {
        @Override
        public void run() {
            super.run();
            while(true){
                consume();
            }

        }

        private void consume() {
            lock.lock();
            try {
                while (queue.size()==0) {
                    System.out.println("队列空,阻塞消费线程");
                    un_empty.await();
                }
                queue.poll();
                System.out.println(Thread.currentThread().getName() + "取走一个元素" + "还剩" + queue.size());
                System.out.println("唤醒生产线程");
                un_full.signal();

            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

    static class Produce extends Thread {
        @Override
        public void run() {
            super.run();
            while(true) {
                produce();
            }
        }
        private void produce() {
            lock.lock();
            try {
                while (queue.size() == 10) {

                    System.out.println("队列已满,生产者被阻塞,等待空间");
                    un_full.await();
                }
                queue.offer(1);
                System.out.println("向队列取中插入一个元素,队列剩余空间:" + (10 - queue.size()));
                un_empty.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }


}

队列空,阻塞消费线程
向队列取中插入一个元素,队列剩余空间:9
向队列取中插入一个元素,队列剩余空间:8
向队列取中插入一个元素,队列剩余空间:7
向队列取中插入一个元素,队列剩余空间:6
向队列取中插入一个元素,队列剩余空间:5
向队列取中插入一个元素,队列剩余空间:4
向队列取中插入一个元素,队列剩余空间:3
向队列取中插入一个元素,队列剩余空间:2
向队列取中插入一个元素,队列剩余空间:1
向队列取中插入一个元素,队列剩余空间:0
队列已满,生产者被阻塞,等待空间
Thread-0取走一个元素还剩9
唤醒生产线程
Thread-0取走一个元素还剩8
唤醒生产线程
Thread-0取走一个元素还剩7
唤醒生产线程
Thread-0取走一个元素还剩6
唤醒生产线程
Thread-0取走一个元素还剩5
唤醒生产线程
Thread-0取走一个元素还剩4
唤醒生产线程
Thread-0取走一个元素还剩3
唤醒生产线程
Thread-0取走一个元素还剩2
唤醒生产线程
Thread-0取走一个元素还剩1
唤醒生产线程
Thread-0取走一个元素还剩0
唤醒生产线程
队列空,阻塞消费线程
向队列取中插入一个元素,队列剩余空间:9
向队列取中插入一个元素,队列剩余空间:8
向队列取中插入一个元素,队列剩余空间:7
向队列取中插入一个元素,队列剩余空间:6
向队列取中插入一个元素,队列剩余空间:5
向队列取中插入一个元素,队列剩余空间:4

blockQueue 实现生产者消费者

package com.company;

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

public class BlockingQueuetest {
    public static void main(String[] args) {
        BlockingQueue<String>blockingQueue = new ArrayBlockingQueue<String>(3);
         new BlockingQueuetest().new Producer(blockingQueue).start();
//         new BlockingQueuetest().new Producer(blockingQueue).start();
//         new BlockingQueuetest().new Producer(blockingQueue).start();
         new BlockingQueuetest().new Consumer(blockingQueue).start();
    }
    class Producer extends Thread{
        private BlockingQueue<String>blockingQueue;

        public Producer(BlockingQueue<String> blockingQueue) {
            this.blockingQueue = blockingQueue;
        }
        public void run(){
            String strArr[] = {
                    "a",
                    "b",
                    "c"
            };
            for(int i = 0;i < 999999999;i++){
                System.out.println(getName()+"生产者准备生产集合元素!");
                try {
                    Thread.sleep(1000);
                    blockingQueue.put(strArr[i%3]);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(getName() + "生产完成"+blockingQueue);
            }



        }
    }
    class Consumer extends Thread{
        private BlockingQueue<String>blockingQueue;

        public Consumer(BlockingQueue<String> blockingQueue) {
            this.blockingQueue = blockingQueue;
        }
        public void run(){
            while ((true)){
                System.out.println(getName()+"消费者准备消费集合元素");
                try {
                    Thread.sleep(2000);
                    blockingQueue.take();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(getName()+"消费完成"+blockingQueue);

            }



        }
    }
}

线程组和未处理的异常

线程组

Java用ThreadGroup来表示线程组,他可以对一批线程组进行分类管理,对线程组的控制等于对这批线程的控制,
默认:如果线程A创建了线程B,线程B属于A线程所在的线程组
,一旦确认了线程所属的线程组就无法改变,直到线程死亡。

ThreadGroup和Thread就好比文件夹与文件的关系,Thread不能独立于ThreadGroup存在,就像所有的文件都会有个目录一样。而ThreadGroup能管理着旗下的ThreadGroup和Thread,也类似于文件夹管理子文件夹和文件。

Thread提供了构造器来设置新的线程属于哪个线程组:

  • Thread(ThreadGroup group,Runnable target)
  • Thread(ThreadGroup group,Runnable target,String name)
  • Thread(ThreadGroup group,String name)
    Thread类不提供setThreadGroup()来改变所属线程组,只提供getThreadGroup()来返回该线程所属的线程组

线程池

我们在用线程时,就去创建一个线程。
如果并法度很高,有些线程创建完了运行完立马结束,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?

在Java中可以通过线程池来达到这样的效果。

ThreadPoolExecutor

Executor框架是一种将线程的创建和执行分离的机制。它基于Executor和ExecutorService接口,及这两个接口的实现类ThreadPoolExecutor展开,Executor有一个内部线程池,并提供了将任务传递到池中线程以获得执行的方法,可传递的任务有如下两种:通过Runnable接口实现的任务和通过Callable接口实现的任务。在这两种情况下,只需要传递任务到执行器,执行器即可使用线程池中的线程或新创建的线程来执行任务。执行器也决定了任务执行时间。

  • 线程池:
    • 可缓存线程池newCachedThreadPool
package com.company;

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

public class ThreadPool {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for(int i = 0;i < 10;i++){
            final int index =  i ;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"的i="+index);
                }
            });


        }
    }
}

  • 定长线程池 newFixedThreadPool
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPool {
    static int count;
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);

        for(int i =0;i < 12;i++){
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "正在运行");
                    try {
                        count++;
                        Thread.sleep(100);
                        System.out.println(count);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }finally {
                        System.out.println(Thread.currentThread().getName()+"运行结束");
                    }
                }
            });
            if(executorService.isTerminated()) System.out.println("全部结束");


        }
    }


}

  • 定长线程池支持定时和周期性任务newScheduledThreadPool
    public static void main(String[] args) {
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
        for(int i =0;i<10;i++) {
            scheduledExecutorService.schedule(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+"Three seconds delay");
                }
            }, 3, TimeUnit.SECONDS);
        }
  • 单线程化线程池newSingleThreadExecutor
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        for(int i = 0;i < 10;i ++){
            final int index = i;
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+" "+index);

                }
            });
        }

这四类线程池类底层都是ThreadPoolExecutor类进行初始化的

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }


    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
  public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());//里面使用了父类的构造函数,下面就是本类和父类的继承关系,看看他的父类是什么,你就明白了
    }

所以所有的线程池都是基于ThreadPoolExecutor实现的


    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }
	/*
 
	corePoolSize:核心池大小,意思是当超过这个范围的时候,就需要将新的线程放到等待队列中了即workQueue;
 
	maximumPoolSize:线程池最大线程数量,表明线程池能创建的最大线程数
 
	keepAlivertime:当活跃线程数大于核心线程数,空闲的多余线程最大存活时间。
 
	unit:存活时间的单位
 
	workQueue:存放任务的队列---阻塞队列
 
	handler:超出线程范围(maximumPoolSize)和队列容量的任务的处理程序
 
	
	我们执行线程时都会调用到ThreadPoolExecutor的execute()方法,现在我们来看看这个方法的源码(就是下面这段代码了,这里面有一些注释解析),我直接来解释一下吧:在这段代码中我们至少要看懂一个逻辑:当当前线程数小于核心池线程数时,只需要添加一个线程并且启动它,如果线程数数目大于核心线程池数目,我们将任务放到workQueue中,如果连workQueue满了,那么就要拒绝任务了。详细的函数我就不介绍了
 */
	
public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /*
         * Proceed in 3 steps:
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task.  The call to addWorker atomically checks runState and
         * workerCount, and so prevents false alarms that would add
         * threads when it shouldn't, by returning false.
         *
         * 2. If a task can be successfully queued, then we still need
         * to double-check whether we should have added a thread
         * (because existing ones died since last checking) or that
         * the pool shut down since entry into this method. So we
         * recheck state and if necessary roll back the enqueuing if
         * stopped, or start a new thread if there are none.
         *
         * 3. If we cannot queue task, then we try to add a new
         * thread.  If it fails, we know we are shut down or saturated
         * and so reject the task.
         */
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }
发布了124 篇原创文章 · 获赞 9 · 访问量 2482
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章