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类:
MyThread类:
3.1.1 实现Runnable接口
Test类:
MyRunnable类:
3.1.1 实现Callable接口
通过Callable + FutureTask创建的线程可以有返回值
Test类:
MyCallable类:
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(),再次中断一次。如图:
3.3 线程的生命周期
如图:
3.4 线程状态
在Thread类中有个一枚举类State中,标明了线程生命周期的执行状态。
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存储当前所属线程值:
3.6.2 使用ThreadLocal实现线程间数据的共享
ThreadLoacl 将变量的访问限制在当前线程内,不允许变量在多个线程间共享,其内部实现是 JVM 为每个线程维护一个 ThreadLocalMap,这个 map 的 key 是 ThreadLocal 实例本身(弱引用),value 是 ThreadLocal 存储的对象。
ThreadLocal 的内存泄漏,其根本原因是 ThreadLocal 的生命周期跟线程一样长,所以如果线程不结束(或没有显式的调用 ThreadLocal.remove() 方法),那 ThreadLocal 就会一直存在,导致 ThreadLocal 中存储的对象得不到回收,因此造成内存泄露。
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()。