博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Java多线程总结
阅读量:7061 次
发布时间:2019-06-28

本文共 15596 字,大约阅读时间需要 51 分钟。

hot3.png

#1 线程介绍 ##1.1 启动线程

1.1.1 新建一个类继承Thread类,重写run()方法。

常规写法

class A extends Thread {		@Override	public void run() {		System.out.println("......");	}		public static void main(String[] args) {		A a = new A();		a.start; // 启动线程	}}

匿名子类写法

class A {	public static void main(String[] args) {		Thread thread = new Thread() {			@Override			public void run() {				System.out.println("......");			}		};		thread.start();	}}

1.1.2 将逻辑定义在Runnable的run()方法中。

常规写法

class A implements Runnable{	@Override	public void run() {		System.out.println("......");	}		public void main(String[] args) {		A a = new A();		Thread thread = new Thread(a);		thread.start();	}}

匿名内部类

class A {    public static void main(String[] args) {        Thread thread = new Thread(new Runnable() {            @Override            public void run() {                System.out.println("......");            }        });		thread.start();    }}

Lambda表达式写法

class A {    public static void main(String[] args) {        Thread thread = new Thread(() -> {            System.out.println("......");        });		thread.start();    }}

##1.2 启动线程方式的比较

Java单继承多实现,所以使用Runnable会更灵活,一般要使用Thread中的方法才会去继承Thread。

##1.3 启动线程的注意点 无论是继承重写run()方法还是实现Runnable接口中的run()方法,在run方法中只是写上你希望新开辟的线程为你做的逻辑。真正启动线程是需要主线程去调用新开辟的线程的start()方法才能启用线程去执行run()方法。

tips:启动JVM后不单只有一个主线程,还有垃圾回收,和内存管理等线程。

#2线程声明周期 ##2.1 Daemon 线程 主线程会从main()方法开始,直到main()方法结束后退出JVM。如果主线程中启动了额外的线程,默认会等待所有线程都执行完run()方法才退出JVM。如果有一个Thread被标记为Daemon线程,在所有非Daemon线程结束后Daemon线程才会结束并且退出JVM。 常用API:设置Daemon线程setDaemon(),判断Daemon是否为Daemon线程isDaemon();

tips:默认从Daemon线程产生的线程是也Daemon线程,所以在生成它的线程结束的时候也一并停止。

##2.2 线程状态图线程状态图以及常用API解释

2.2.1 线程状态图

调用线程的start()方法后,一般有三种状态Runnable(就绪)、Running(运行)、Blocked(阻塞)。 当线程执行sart()方法后线程进入Runnable(状态)等待Scheduler(排班器)排入CPU执行,线程才会执行run()方法进入Running(运行)状态。 CPU同一个时间点上只会执行一个线程,但CPU会不断切换线程工作,这样使CPU实现了并行操作,这样使得线程看起来是同时执行的。

tips: 串行操作(按照执行顺序一个个来),并行操作(一会执行这,一会执行那),并发(需要多核CPU每个CPU处理分别处理一个操作)。

线程可以设置优先权通过setPriority()方法,默认5,范围1-10,数值越大,Scheduler(排版器)优先排入CPU执行。

Blocked(阻塞)一般有以下集中情况:

1 Thread.sleep() 线程休眠状态,此时线程是不会释放锁,当线程醒来(自然醒或者interrupt()唤醒)还是带着锁的,然后等到Scheduler排到他然后继续执行。

2 进入synchronized前竞争对象锁的阻断

3 调用线程wait()方法,线程释放对象锁进入对象的等待集合,当notify()通知他的时候,等待Scheduler排到他执行并再次去竞争锁。

4 等待输入/输出完成,读写大文件此时CPU是在阻塞在等待操作系统完成IO操作。

tips:当一个线程进入Blocked状态,让一个线程排入到CPU执行(成为Running状态),避免CPU空闲,是运用多线程改进效能的方式之一。

2.2.2 常用API-join() A线程正在运行,流程中使用join()操作将B线程加入,那么等到B线程执行完毕后再执行A线程。 join(xxxms)表示当插入的线程至多处理xxxms如果xxxms到了插入线程还没执行完毕,那么目前线程可继续执行原本工作。

2.2.3 停止线程

线程完成run方法后,就会进入Dead,已经调用过的start()方法的线程不能再次调用。 stop()方法,将不理会所设定的释放、取得锁流程,线程会直接释放所有已锁定的对象,这有可能使对象陷入无法预期的状态,除了stop()方法外,Thread的resume()、suspend()、destroy()等方法也已经Deprecated。

停止线程最好自行操作,让线程跑完应有流程而不是用stop()方法。

class A implements Runnable {    private boolean flag = true;    public void stop() {        this.flag = false;    }        @Override    public void run() {        while (flag) {            System.out.println("......");        }    }}

2.2.4 ThreadGroup

每个线程都属于某个ThreadGroup,如在main()方法中产生一个线程那么就属于main线程群组。Thread.currentThread().getThreadGroup().getName();获得群组名。每个线程产生的时候都会归入某个线程群组中,可以指定行线程群组,不指定就归入产生该子线程的线程群组,一旦指定线程群组就无法更换。ThreadGroup的interrup、setMaxPriority()对群组中所有的线程都有作用。

当ThreadGroup中的线程发生异常,执行顺序如下:

1 ThreadGroup如果有父ThreadGroup那么调用父ThreadGroup的uncaughtException()方法。

2 看Thread中是否使用setUncaughtExceptionHandler()方法设定Thread.uncaughtExceptionHandler()实例,调用其uncaughtException()方法。

3 看下异常是否为ThreadDeath实力,是的话什么都不处理,不是的话打印堆栈。

可以重写ThreadGroup.uncaughtException以及Thread中的uncaughtException()这两个方法来实现自己所需要的异常控制。

2.2.5 synchronized 与 volatile

每个对象都会有个内部锁(Intrinsic Lock)或叫监控锁(Monitor Lock)。 任何线程要执行synchronized区块都必须先获得指定对象的锁。如果A线程取得对象锁开始执行synchronized区块,B线程也想执行synchronized区块,但因无法拿到对象锁而进入等待状态(Blocked),直到A线程释放锁(如执行完synchronized区块,或者A线程被wait()了),B线程才有机会去取得锁而执行synchronized区块。线程若因尝试执行synchronized区块而进入Blocked,在取得锁之后,会先进入Runnable状态,等待CPU的scheduler排入Running状态。

synchronized放置的几种位置

1 放在方法上(粗粒度)

public synchronized void add(Object object) {        System.out.println("运行逻辑");    }

2 代码块(相对细粒度)

public void add(Object object) {        // ...        synchronized (this) {            System.out.println("运行逻辑");        }        // ...    }

3 对于不是线程安全的类的方法

ArrayList
arrayList = new ArrayList<>(); Thread thread = new Thread(() -> { // ... synchronized (arrayList) { System.out.println("运行逻辑"); } // ... });

4 使用Collections中的synchronizedCollection()、synchronizedList()、 synchronizedSet()、synchronizedMap()将传入的Collection、List、Set、Map、对象打包返回线程安全的对象

将上面的代码简化

List
list = Collections.synchronizedList(new ArrayList<>()); // ... list.add(Integer.valueOf("....")); // ...

使用synchronized时,可以通过不同对象做为锁的来源实现更细粒度的控制。

class A
{ private int data1 = 0; private int data2 = 0; private E e1; private E e2; public void doSome() { System.out.println("其他逻辑"); synchronized (e1) { data1++; } System.out.println("其他逻辑"); } public void doOther() { System.out.println("其他逻辑"); synchronized (e2) { data2++; } System.out.println("其他逻辑"); }}

此时doSome()和doOther()不会同时被两个以上的线程执行,并且data1与data2不同时出现在两个方法中,不会引发内存共享问题,并且此时不同方法提供不同对象做为锁的来源,这样不会导致一个线程在拿到锁之后执行doSome()中synchronized代码块,另一个线程在doOther()中的synchronized那发生获取锁的等待问题了。

Java中的synchronized提供可重入同步(Reentrant Synchronized),线程在获取某对象锁定后,在执行的过程中又要执行synchronized,此时取得锁对象是一个的话,那么可以直接执行。

死锁:当一个线程获取A对象锁之后需要获取B对象锁时,假设另一个线程获取B对象锁之后需要获取A对象锁,此时第一个线程因为要获取B对象锁进入Blocked,第二个线程要获取A对象锁也进入Blocked。

2.2.5 使用volatile

synchronized对所标志区块具有互斥性与可见性,互斥行:synchronized区块同一时间只有一个线程,可见性:线程离开synchronized区块后,另一个线程所接触到的就是上一个线程改变后的对象状态。

在Java中对可见性的要求,可以使用volatile达到变量范围。

正常情况下线程可以快取变量值,线程可以从共享内存中快取变量值放到自己的内存空间,然后将自己内存空间中快取的值输出出去。这可能导致当线程A快取一个值data之后,这个值data被线程B所改变,但此时A不会再取共享内存空间中拿到变化之后的值data而是直接拿自己内存空间中快取的值olddata输出出去。

此时可以在变量上声明volatile,表示变量不稳定,可能在多线程下存取,这保证了变量的可见性,当有线程变动了变量值,另一个线程可看到变化。被volatile声明的变量,不允许线程快取,变量值的存取一定是在共享内存中进行的。

volatiole保证的是单一变数的可见行,线程对变量的存取一定是在共享内存中的,不会在自己的内存空间中快取变量,线程对共享内存中变量的存取,另一个线程一定看得到。

2.2.6 等待与通知

wai() notify() notifyAll() 是Object定义的方法,通过这三个方法控制线程释放对象的锁,或者通知线程参与锁的竞争。

线程进入synchronized范围前,需要先获得对象的锁。执行synchronozed范围的程序代码期间,若调用锁定对象的wait()方法,线程会释放对象的锁,进入此对象的等待集合(WaitSet)而处于Blocked,此时其他线程可以竞争对象锁,拿到锁之后执行synchronized代码块。

tips: 调用wait()方法的时候必须锁定该对象。

放在等待集合的线程不参与CPU排班,wait()可以指定时间,时间到之后线程进入排班,如果指定时间为0或者不指定,则线程持续等待,知道被中断(interrupt())或是notify()可以参与排班。

tips: 因为wait()后的线程处于Blocked不是Runnable所以是不能进入CPU排班

被竞争锁定的对象调用notify()时,会从对象等待集合中随机通知一个线程加入排班,再次执行synchronized前,被通知的线程会和其他线程功能竞争对象锁;如果调用notifyAll(),所以等待集合中的线程都会参与排班,这些线程会与其他线程共同竞争对象锁。线程调用wait()方法进入等待当时间到或notify(),进入排班并取得对象锁之后再从调用wait()处开始执行。notifyAll()同理

##2.3 并行API

2.3.1 java.util.concurrent包中的Lock、ReadWriteLock、Condition

Lock:

class A {    private Lock lock = new ReentrantLock(); // 声明类全局变量lock 一个对象一个锁     // 类似于一个对象一个synchronized        public void doSome() {        lock.lock(); // 上锁        try {            // ...        } finally {            lock.unlock(); // 一定要释放锁 并且为了释放锁成功要放在finally中        }    }}

ReentrantLock可以达到synchronized的作用,如果已经有线程取得Lock对象锁定,尝试再次锁定同一Lock对象是可以的。锁定Lock对象可以调用其lock()方法,只有取得Look对象的锁定的线程,才可以往下执行,接触锁定调用Lock对象的unlock方法。

Lock接口还定义了tryLock()方法,如果线程调用tryLock()可以取得锁定那么返回true,若无法取得锁定不会发生阻断而是返回false。Lock接口还有isHeldByCurrentThread()方法返回true,false。

ReadWriteLock:

ReentrantReadWriteLock.ReadLock操作lock()方法时,若没有任何ReentrantReadWriteLock.WriteLock调用lock()方法,也就是没有任何写入锁定的时候,就可以取得读取锁定(读取锁可以同时被多个线程拿到)。 ReentrantReadWriteLock.WriteLock调用lock()方法时,若没有任何 ReentrantReadWriteLock.ReadLock或ReentrantReadWriteLock.WriteLock调用过lock()方法,也就是没有任何读取或者写入锁定的时,才可以取得写入锁。

读锁可多线程,在获取读锁之前没有任何写锁才能获取读锁。 在没有任何读锁和写锁的时候才能获取写锁。使用读写锁使得读写操作分离,增加读取效率。

class A
{ private ReadWriteLock lock = new ReentrantReadWriteLock(); private E data; public E get() { // lock.readLock() 返回 ReentrantReadWriteLock.ReadLock// ReentrantReadWriteLock.ReadLock readLock = (ReentrantReadWriteLock.ReadLock) lock.readLock(); lock.readLock().lock(); try { return data; } finally { lock.readLock().unlock(); } } public void set(E e) { // lock.writeLock() 返回 ReentrantReadWriteLock.WriteLock// ReentrantReadWriteLock.WriteLock writeLock = (ReentrantReadWriteLock.WriteLock) lock.writeLock(); lock.writeLock().lock(); try { this.data = e; } finally { lock.writeLock().unlock(); } }}

StampedLock:

ReadWriteLock在没有任何读取或写入锁定时,才可以取得写入锁定,这可以用于实现悲观读取(Pessimistic Reading)。

然而当读取线程多,写入线程少的时候,使用ReadWriteLock可能使得写线程处于Starvation(饥饿)状态,因为写入锁可能迟迟无法竞争到锁,而处于等待状态。此时JDK8中增加了StampedLock类,可支持乐观读取(Optimistic Reading),也就是当读取线程多,写入线程少,可以乐观的认为写入与读取同时发生的机会较少。因此不悲观地使用完全的读取锁定,程序可以查看数据读取之后,是否遭到写入线程的变更,再采取后续措施(重新读取变更后的数据,或者抛出例外)。

class A
{ private StampedLock lock = new StampedLock(); private E data; public E get() { long stamp = lock.tryOptimisticRead(); // 试着乐观读取锁(不会真正执行读取锁定) // 1 返回stamp给validate用 2 如果已经有排他锁返回0 E res = data; if (!lock.validate(stamp)) { // 查询是否有排他所的锁定 1 没有排他锁返回true 2 stamp是0的话返回false // 3 stamp表示当前已持有的锁返回true 4 戳记stamp被其他排他锁获取返回false stamp = lock.readLock(); // 使用readLock()做真正的读取锁定 try { res = data; // 在锁定的情况下更新局部变量 } finally { lock.unlockRead(stamp); // 解除读取锁定 } } return res; // 没有其他排他锁直接返回变量 } public void set(E e) { long stamp = lock.writeLock(); try { this.data = e; } finally { lock.unlockWrite(stamp); } }}

tips: 在validate()之后发生写入而返回结果不一致是有可能的,如果此时需要保证数据一致性,那么应该使用悲观读取。

Condition:

Condition接口用来搭配Lock,实现与Object的wait(),notify(),notifyAll()相同的效果。分别是Condition的await(),signal(),signalAll()。 当多线程操作统一对象的时候,Object的wait()会让不同类型的线程多在此对象的等待集合中等待,当notify()通知的时候是随机通知的。但使用Conditon我们可以创建多个Condition对象,当使用await()方法那么就可以到指定的Condition的等待集合中,通知的时候也可以到指定的Condition对象的等待集合中通知。

class A
{ private Lock lock = new ReentrantLock(); private Condition condition1 = lock.newCondition(); private Condition condition2 = lock.newCondition();}

Executor:

Runnable用来定义可执行流程与可使用数据,Thread用来执行Runnable。将Runnable指定给Thread创建用,并调用start()方法。 java.util.concurrent.Executor接口,出现在JDK1.5,目的是将Runnable的指定与实际如何执行分离。

Java线程池的操作方法定义在Executor的子接口ExecutorService(java.util.concurrent.ExecutorService)中,由抽象类AbstractExecutor实现,如果需要使用线程池功能的话可以使用其子类java.util.concurrent.ThreadPoolExecutor,ThreadPoolExecutor有多种构造方法。

class A {    public void doSome(Executor executor) {        System.out.println("执行逻辑");    }    public static void main(String[] args) {        // 使用newFixedThreadPool创建线程的时候指定线程数量//        Executors.newFixedThreadPool(5);        // java.util.concurrent.Executors 的 newCachedThreadPool() 创建ThreadPoolExecutor        // 使用上面的方法创建线程池会在必要的时候建立,Runnable可能执行在新建的线程上,也可能在重复利用的线程中        ExecutorService executorService = Executors.newCachedThreadPool();        new A().doSome(executorService);        // shutdown()方法会在指定执行的Runnable都完成后将ExecutorService(此处是ThreadPoolExecutor)关闭        executorService.shutdown();    }}

IDEA不推荐上面两种创建方法理由如图。

Future与Callable:

ExecutorService中的submit(),invokeAll(),invokeAny()中用到了java.util.concurrent.Future以及Callable。

Future:将想执行的工作交给Future,Future会使用另外一个线程来工作,此时你可以去忙别的事情,过些时候再调用Future.get()方法取得结果,如果结果产生了,get()直接返回结果,否则进入阻断状态直到结果返回。get()还可以指定等待结果的时间,若时间到还未产生结果则抛出java.util.concurrent.TimeoutException,也可以使用Future的isDone()方法查看是否有结果产生。

Future经常与Callable搭配使用,Callable与Runnable相似但有两点不同Runnable的run()方法无法产生返回值,无法抛出收检异常(如Thread.sleep的异常一定要在run()中try catch。FutureTask创建的时候可以接受Runnable或者Cabble两种。

class A {    static String getStr() throws Exception {        Thread.sleep(5000);        return "Lambda表达式的测试方法执行完成";    }    public static void main(String[] args) throws Exception{        FutureTask
futureTask = new FutureTask
(new Callable
() { @Override public String call() throws Exception { Thread.sleep(8000); return "内部匿名类的测试方法执行完成"; } }); FutureTask
futureTask2 = new FutureTask
(A::getStr); // FutureTask也实现了Runnable接口所以也可以给Thread创建实力用 new Thread(futureTask).start(); new Thread(futureTask2).start(); while (!(futureTask.isDone() && futureTask2.isDone())) { System.out.println("我去干其他事情了"); } System.out.println(">>>>两个FutureTask都完成了" + "FutureTask1:" + futureTask.get() + "FutureTask2:" + futureTask2.get()); }}

使用线程池的submit方法返回Future让我稍后取得结果。

class A {    static String setChicken(int num) throws InterruptedException {        // 用线程休眠10秒表示 老板再做30份烤鸡        Thread.sleep(5000);        return "老板做了" + num + "只烤鸡";    }    public static void main(String[] args) {        ExecutorService service = Executors.newCachedThreadPool();        System.out.println("老板我要30份烤鸡");        // ExecutorService的submit()方法接口Callable对象,调用后返回Future对象        // 为了让你在稍后可以取得运算结果        // 这边做30个烤鸭比上面做30个烤鸡快,上面是Thread.start()后执行Future,Future使用        // 单个新线程来做30个烤鸡,但这边使用线程池,相当于好多个线程一起做30个烤鸡        Future
futureTask = service.submit(() -> setChicken(30)); while (!futureTask.isDone()) { System.out.println("我去干其他的了"); } System.out.println("老板的30份烤鸡好了"); }}

ScheduledThreadPoolExecutor:

ScheduledExecutorService的实现类ScheduledThreadPoolExecutor并且继承ThreadPoolExecutor,具有线程池和排与线程排程功能。 可以使用Executors。

class A {    public static void main(String[] args) {        // 单个线程排程        ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();        service.scheduleWithFixedDelay(() -> {            System.out.println(new Date());            try {                Thread.sleep(2000); // 假设这个工作会进行两秒            } catch (InterruptedException e) {                e.printStackTrace();            }        }, 2000, 1000, TimeUnit.MILLISECONDS); // 线程进入后等待2S开始排程工作        // 上一个线程结束后等待1S继续下个线程        service.scheduleAtFixedRate(() -> {            System.out.println(new Date());            try {                Thread.sleep(2000); // 假设这个工作会进行两秒            } catch (InterruptedException e) {                e.printStackTrace();            }        }, 2000, 1000, TimeUnit.MILLISECONDS); // 线程进入后等待2S开始排程工作        // 上一个线程结束后等待1S继续下个线程,如果下个线程在1S后没完成,那么等线程完成工作后直接执行        // 下一个线程    }}

ForkJoinPool:

Future的一个实现类java.util.concurrent.ForkJoinTask及其子类有ExecutorService另一个实现类java.util.concurrent.ForkJoinPool有关,他们主要用来解决(Divide and Conquer) 分而治之的问题。 ForkJoinTask在ForkJoinPool管理的时候执行fork()方法,则会以另外一个线程来执行他,调用join()取得结果,无结果则等待至结果返回。一般使用ForkJoinTask的子类RecursiveTask和RecursiveAction, 用类继承两个的其中一个,有返回值用RecursiveTask,没返回值用 RecursiveAction,调用的时候使用他们的compute方法。

class A extends RecursiveTask
{ private long n; public A(long n) { this.n = n; } @Override protected Long compute() { // 操作compute()方法 将子任务的分解和求解放到compute()中 if (n <= 20) { // 避免分解出过多的子任务造成负担。 return usualSolve(n); // 小于20的不分解,直接循环运算 } // 分解出n-1子任务请ForkJoinPool分配线程来执行 ForkJoinTask
subTask = new A(n - 1).fork(); // 分解n-2子任务并且直接运行 + 取得子任务执行结果。 return new A(n - 2).compute() + subTask.join(); } private long usualSolve(long n) { if (n <= 1) { return n; } return usualSolve(n - 1) + usualSolve(n - 2); } public static void main(String[] args) { A a = new A(45); ForkJoinPool pool = new ForkJoinPool(); // 所有ForkJoinTask实力的compute()方法执行完之后,ForkJoinPool就会关闭 System.out.println(pool.invoke(a)); // 开始分而治之 }}

转载于:https://my.oschina.net/u/3544267/blog/2243800

你可能感兴趣的文章
专家提醒:填报高考志愿不能过分依赖“大数据”
查看>>
河北汽车客运站异地联网售票!那些你不知道的事
查看>>
我们需要什么样的威胁情报分析师?
查看>>
超过24%!天合光能IBC电池效率再创新高
查看>>
MySQL两千万数据优化&迁移
查看>>
《UNIXLinux程序设计教程》一2.3 打开和关闭流
查看>>
《Android的设计与实现:卷I》——第2章 2.5 JNI操作Java对象
查看>>
FBI和DHS在响应网络攻击时为何存在分歧
查看>>
《系统分析与设计方法及实践》一3.1 案例研究中涵盖的内容
查看>>
湖北省开启能源供给侧改革 将重点发展光伏发电
查看>>
美国伊利诺伊州响应区块链技术倡议活动 将举办黑客马拉松
查看>>
阿里音乐打算用大数据发掘下一个TFboy
查看>>
大数据能否成帮助人类 控制寨卡病毒疫情
查看>>
大数据为推动中国出版“走出去”提供新机遇
查看>>
Nest应用登陆tvOS 可在大屏幕上进行监控了
查看>>
Tumblr遭遇数据泄露 6000多万名用户受波及
查看>>
新的系统漏洞使iOS10更容易被攻破,苹果称已开始修复
查看>>
物联网路径,美国运营商怎么走?
查看>>
除Hadoop大数据技术外,还需了解的九大技术
查看>>
印度限制官员使用智能手机:真因为怕中国黑客?
查看>>