转载

Java线程基础-多线程

1、是什么

1.1 什么是线程

线程是进程中执行流的最小单位。一个进程中可以由多个线程。

1.2 CPU与线程的关系

CPU的核心数与线程数是 1: 1的关系,例如一个 4 核的CPU同时可以支持4个线程同时运行,但在引入超线程的技术以后,关系变成 1:2了,也就是说一个4核的CPU可以同时运行8个线程。

CPU的一个核,在同一时刻只能运行一个线程,当有多个线程的时候,CPU会分片给每个线程一个执行时间片,分片给A线程时间片后执行A线程,时间到了后记录执行的位置和内容,切换去执行B线程,如此来来回回的切换,只是时间非常短,我们感觉不到而已。

1.3 进程与线程的关系

进程:一个程序运行,就会启动一个进程,用于程序调度和运行的资源分配。一个进程内部可以由多个线程,多个线程之间共享这个进程的资源。进程与进程之间是相互独立的。

线程:线程依附于进程运行,是一个进程执行流的最小单元。

举例:

我们安装了迅雷,当我们启动迅雷的时候,后台就启动了迅雷的一个进程,当我们去下载文件的时候,或下载多个的时候,就是开启的线程在下载。

2、为什么

还是拿说迅雷,迅雷为什么下载这么快,有一个原因就是,当我们下载一个文件的时候,迅雷默认是开启 5 个线程去下载的,这就是多线程下载。

多个线程并发,提高了CPU的利用率,比单线程的效率更高。所以在开发中,我们会进程使用多线程。

3、怎么用

3.1 创建线程

Java创建线程的方式有三种:

1、继承Thread类

2、实现Runnable接口

3、实现Callable

3.1.1 继承Thread

Test类:

Java线程基础-多线程

MyThread类:

Java线程基础-多线程

3.1.1 实现Runnable接口

Test类:

Java线程基础-多线程

MyRunnable类:

Java线程基础-多线程

3.1.1 实现Callable接口

通过Callable + FutureTask创建的线程可以有返回值

Test类:

Java线程基础-多线程

MyCallable类:

Java线程基础-多线程

3.2 线程停止

线程结束的方法:

1、方法执行完成自动终止

2、抛出异常,又没捕获异常

3、调用线程停止方法:interrupt(),isinterrupted()以及静态方法interrupted(). 和 stop()方法,不过stop()方法已经标记为 @Deprecated,所以不建议使用了,正确的姿势其实是调用 interrupt() 方法。

区别:

interrupt() :通知线程应该中断了,给线程设置一个中断标志,但实际该线程仍会继续运行。具体来说就是:

如果线程处于阻塞状态(如:sleep、wait、join),那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。为了能够安全的停止线程,这个时候线程的中断标志会被复位成为false,所以这个时候我们应该在catch里面再调用一次interrupt(),再次中断一次。

如果线程处于正常活动状态,那么会将该线程的中断标志设置为true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响。

interrupt() 并不能真正的中断线程,需要被调用的线程自己进行配合才行,在正常运行任务时,经常检查本线程的中断标志位,如果被设置了中断标志就自行停止线程。具体到底中断还是继续运行,应该由被通知的线程自己处理。

isinterrupted():检查当前中断标识(即查看当前中断信号是true还是false)。

interrupted():检查当前中断标识(即查看当前中断信号是true还是false),并清除中断信号。一般处理过中断以后使用此方法。

stop():会真的杀死线程,不给线程喘息的机会,如果线程持有 ReentrantLock 锁,被 stop() 的线程并不会自动调用 ReentrantLock 的 unlock() 去释放锁,那其他线程就再也没机会获得 ReentrantLock 锁,这实在是太危险了。所以该方法就不建议使用了,类似的方法还有 suspend() 和 resume() 方法,这两个方法同样也都不建议使用了,而interrupt() 方法仅仅是通知线程。

安全的停止线程:

在主线程中调用了 interrupt() 后,需要再run中在catch里面再调用一次interrupt(),再次中断一次。如图:

Java线程基础-多线程

3.3 线程的生命周期

如图:

Java线程基础-多线程

3.4 线程状态

在Thread类中有个一枚举类State中,标明了线程生命周期的执行状态。

Java线程基础-多线程

Java 语言里的线程生命周期:

New: 新建,即我们执行了 new Thread() 之后,该线程就处于新建状态,此时仅由JVM为其分配内存,并初始化其成员变量的值。

Runnable: 就绪状态,即我们执行了 start() 方法的状态,Java虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。

Running: 运行状态,即处于就绪状态的线程获得了CPU,开始执行run()方法的线程执行体,则该线程处于运行状态。

Blocked: 阻塞状态,线程遇到synchronized 修饰的方法、代码块同一时刻只允许一个线程执行,其他线程只能等待,这种情况下,等待的线程就会从 Runnable转换到 Blocked状态。

Waiting: 无时限等待状态,

Timed_Waiting: 有时限等待状态

Terminated: 终止状态

3.5 线程的常用方法及特性

3.5.1 wait()

wait()方法是Object的静态方法,调用该方法后,线程进入Waiting状态,同时会释放对象的锁。因此,wait方法一般用在同步方法或同步代码块中。

wait(long)方法会导致线程进入Timed_Waiting状态。

3.5.2 sleep()

sleep(long)方法导致当前线程休眠,线程休眠会交出CPU,让CPU去执行其他的任务;线程进入Timed_Waiting状态,但不会释放当前占有的锁。即当前线程持有某个对象锁时,即使调用sleep()方法其他线程也无法访问这个对象,等到预计时间之后再恢复执行。

3.5.3 yield()

yield()方法会使当前线程让出CPU执行时间片,线程进入Runnable状态,也不会释放锁,yield()方法只能让拥有相同优先级的线程获取CPU执行的机会。一般情况下,优先级高的线程有更大的可能性成功竞争得到CPU时间片,但这又不是绝对的,有的操作系统对线程优先级并不敏感。

3.5.4 interrupt()

interrupt()方法目的给这个线程一个中断通知信号,会修改这个线程内部的一个中断标识位。(上面已经详细讲过了,这里就不在说了。)

3.5.4 join()

join方法等待线程终止,如果在线程A上下文中执行了线程B.join()语句,其含义是线程B执行结束后,join()方法才会返回,线程A才可继续执行。

3.6 线程间的共享

当使用多线程时,当多个线程同时操作同一个变量时,由于竞争条件可能破坏该变量的状态,导致线程安全问题(共享数据的一致性)。

确保线程安全,最有效的方法是减少(或避免)多线程之间的数据(状态)共享,对于共享的数据(状态),则应保证在同一时刻对各线程的可见性(visible),保证一致性常用的方发包括:

1、使用ConcurrentHashMap绑定线程和数据的关系。

2、使用 ThreadLoacl<T> 将数据限制在一个线程内,从而避免在多线程之间共享数据。

3、通过 volatile 修饰单一数据或使用 java.util.concurrent.atomic 来操作数据

4、锁机制

3.6.1 使用ConcurrentHashMap 实现线程间数据的共享

定义静态的 ConcurrentHashMap 集合,集合的Key为Thread ,Value存储当前所属线程值:

Java线程基础-多线程

Java线程基础-多线程

3.6.2 使用ThreadLocal实现线程间数据的共享

ThreadLoacl 将变量的访问限制在当前线程内,不允许变量在多个线程间共享,其内部实现是 JVM 为每个线程维护一个 ThreadLocalMap,这个 map 的 key 是 ThreadLocal 实例本身(弱引用),value 是 ThreadLocal 存储的对象。

ThreadLocal 的内存泄漏,其根本原因是 ThreadLocal 的生命周期跟线程一样长,所以如果线程不结束(或没有显式的调用 ThreadLocal.remove() 方法),那 ThreadLocal 就会一直存在,导致 ThreadLocal 中存储的对象得不到回收,因此造成内存泄露。

Java线程基础-多线程

3.6.3 使用Volatile实现线程间数据的共享

Jvm内存模型有三大特性:原子性、可见性、有序性,而Volatile保证了可见性、有序性,但没有保证原子性。

在使用Volatile共享数据的时候,需要保证对变量的写操作不依赖于当前的值,如:i++等。

3.6.4 使用Atomic实现线程间数据的共享

Java 的 java.util.concurrent.atomic 支持 CAS 操作,支持并发下的原子操作,使用可以在多线程中使用。

3.6.5 使用锁机制实现线程间数据的共享

Java 的锁机制既可以保证数据的可见性,也可以保证对数据操作的原子性,Java 通过 synchronized 块和 Lock 和 ReadWriteLock 接口来提供锁机制。

synchronized 块是 Java 的内置加锁机制,一个 synchronized 块包括一个锁对象和一段由其保护的代码块,对 synchronized 方法,其持有的锁对象是方法所在的对象,如果是 static 方法,则其持有的锁对象是其 Class 对象;

synchronized 块具有以下特点:

1)互斥性,同一时间只有一个线程可以获得锁

2)允许同一线程可以重入synchronized 块,避免继承关系中的死锁。

synchronized 块具有一下局限性:

1)如果一个获取到锁的线程由于其他原因(比如:等待IO、调用sleep()方法了)被阻塞了,但是又没释放锁,其他线程就只能继续等待,影响了程序执行效率。

2)当多个线程读写文件时,读操作与写操作会发生冲突,写操作也写操作会发生冲突,但是,读操作与读操作不会发生冲突,但synchronized 块一次只允许一个线程进入,其他线程只能等待。

3.7 线程间的协作

Java中线程通信协作的最常见的两种方式:

1、syncrhoized加锁的线程的Object类的wait()/notify()/notifyAll()

2、ReentrantLock类加锁的线程的Condition类的await()/signal()/signalAll()(推荐使用第2中,因为这种方式更安全更高效)

线程间直接的数据交换:

1、通过管道进行线程间通信:1)字节流;2)字符流

3.7.1 ReentrantLock类加锁的线程的Condition类

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

Condition:

1)Condition是个接口,基本的方法就是await()和signal()方法;

2)Condition依赖于Lock接口,生成一个Condition的基本代码是lock.newCondition()

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

Conditon中的await()对应Object的wait();

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

Condition中的signalAll()对应Object的notifyAll()。

原文  https://segmentfault.com/a/1190000021345631
正文到此结束
Loading...