java多线程 02
多线程
1. 线程高级
1.1 线程的状态
线程状态 | 导致状态发生条件 |
---|---|
NEW(新建) | 线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread只有线程对象,没有线程特征。 |
Runnable(可运行) | 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。调用了t.start()方法 :就绪 |
Blocked(锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。 |
Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。 |
Timed Waiting(计时等待) | 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。 |
Teminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。 |
1.2 线程中断
一个线程在执行完任务后会自动结束,如果在运行过程中发生异常,也会提前结束。
1.2.1 interrupt()
将调用该方法的对象所表示的线程表记一个停止标记,并不是真的停止该线程。
1.2.2 interrupted()
获取当前线程的中断状态,并且会清楚线程的状态表记,是一个静态方法。
1.2.3 isInterrupted()
获取调用方法的对象所表示的线程,不会清楚线程的状态标记。
1.3 线程分类
daemon线程(守护线程),比如垃圾回收线程,Java虚拟机不会等待此类线程结束就自己退出。
//创建守护线程
public static void main(String[] args) {
Thread daemonThread = new Thread(new Runnable() {
public void run() {
}
});
//设置为守护线程
daemonThread.setDaemon(true);
daemonThread.start();
}
user线程(用户线程),只要有一个用户线程还没结束,正常情况下JVM就不会退出。
通常自定义的线程都属于用户线程
public static void main(String[] args) {
Thread thread = new Theread(new Runnable() {
public void run() {
}
});
//设置守护线程
thread.setDaemon(true);
//启动子线程
thread.start();
System.out.print("main thread is over");
}
1.4 volatie关键字
1.4.1 JVM
概述:JMM(Java Memory Model)Java内存模型,是java虚拟机规范中所定义的一种内存模型。
Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将
变量存储到内存和从内存中读取变量这样的底层细节。
所有的共享变量都存储于主内存。这里所说的变量指的是实例变量和类变量。不包含局部变量,因为局部变
量是线程私有的,因此不存在竞争问题。每一个线程还存在自己的工作内存,线程
的工作内存,保留了被线程使用的变量的工作副本。线程对变量的所有的操作(读,取)都必须在工作内存中
完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问
对方工作内存中的变量,线程间变量的值的传递需要通过主内存完成。
1.4.2 volatile关键字
private volatile boolean flag ;
- VolatileThread线程从主内存读取到数据放入其对应的工作内存
- 将flag的值更改为true,但是这个时候flag的值还没有写会主内存
- 此时main方法main方法读取到了flag的值为false
- 当VolatileThread线程将flag的值写回去后,失效其他线程对此变量副本
- 再次对flag进行操作的时候线程会从主内存读取最新的值,放入到工作内存中
总结: volatile保证不同线程对共享变量操作的可见性,也就是说一个线程修改了volatile修饰的变量,
当修改写回主内存时,另外一个线程立即看到最新的值。
但是volatile不保证原子性。
1.4.3 volatile与synchronized
volatile只能修饰实例变量和类变量,而synchronized可以修饰方法,以及代码块。
volatile保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全);而synchronized
是一种排他(互斥)的机制,
2. 并发编程
多个线程确实能快速的处理任务,高效利用cpu资源。但是也会带来一些列问题,常见问题有:
1、因为多个线程共享同一个内存空间导致的数据安全问题
2、因为要同步线程而导致多个线程相互竞争锁时产生死锁问题
3、系统需要切换线程来满足线程交替执行,所以如果频繁切换线程就会导致大部分cpu时间用在了切换线程上,而出现效率问题。
2.1 数据安全问题
共享资源
所谓共享资源,就是同一份资源被多个线程所持有或者多个线程访问。线程安全问题是指当多个线程同时读写一个共享资源并且没有任何同步措施时,导致出现脏数据或者其他不可预见的结果的问题。
因此在访问共享资源时需要保证同步,保证被操作资源(如变量)的原子性。
private static int shared = 0;
private static void incrShared(){
//自增操作会分为三步完成,
//第一:把shared的数据读取到cpu寄存器中
//第二:把shared的值加1
//第三:把自增后的值写回到内存
shared++;
}
static class ChildThread extends Thread {
@Override
public void run() {
incrShared();
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new ChildThread();
Thread t2 = new ChildThread();
t1.start();
t2.start();
System.out.println(shared);
}
public class ShareVariable {
public static int a = 0;
public static void main(String[] args) throws InterruptedException {
for(int i = 0; i < 1000; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
a++;
}
}).start();
}
Thread.sleep(10000);
System.out.println(a);
}
}
//给自增操作加锁,能够保证操作的原子性,但是性能会降低
private synchronized static void incrShared(){
}
线程同步 sychronized
java中使用关键字synchronized来实现线程的同步,该关键字对应两条jvm指令,monitor enter 和 monitor exit
0: aconst_null
1: astore_1
2: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
5: aload_1
6: invokevirtual #3 // Method java/lang/String.length:()I
9: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
12: return
同步方式
- 方式一:同步代码块
- 方式二:同步方法
同步成员方法
public void m1() {
synchronized(this) {
//代码块
}
}
//以上同步方式等价于如下
public synchronized void m2() {
}
//同步静态代码块
public static void m1() {
synchronized(this) {
//代码块
}
}
//以上同步方式等价于如下
public synchronized static void m2() {
}
synchronized块是Java提供的一种原子性内置锁,Java中的每个对象都可以把它当作一个同步锁来使用,这些Java内置的使用者看不到的锁被称为内部锁,也叫作监视器锁。线程的执行代码在进入synchronized代码块前会自动获取内部锁,这时候其他线程访问该同步代码块时会被阻塞挂起。拿到内部锁的线程会在正常退出同步代码块或者抛出异常后或者在同步块内调用了该内置锁资源的wait系列方法时释放该内置锁。内置锁是排它锁,也就是当一个线程获取这个锁后,其他线程必须等待该线程释放锁后才能获取该锁。另外,由于Java中的线程是与操作系统的原生线程一一对应的,所以当阻塞一个线程时,需要从用户态切换到内核态执行阻塞操作,这是很耗时的操作,而synchronized的使用就会导致上下文切换。
线程活跃问题
线程死锁
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的互相等待的现象,在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。
//死锁示例
public class TestDeadLock {
public static void main(String[] args) {
Boy boy = new Boy();
Girl girl = new Girl();
boy.start();
girl.start();
}
}
class MyLock{
static Object left = new Object(); //左筷子
static Object right = new Object(); //右筷子
}
class Boy extends Thread{
@Override
public void run() {
synchronized (MyLock.left) { //拥有左筷子 锁
System.out.println("boy获取到了左筷子,等待右筷子");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (MyLock.right) {
System.out.println("boy可以吃饭");
}
}
}
}
class Girl extends Thread{
@Override
public void run() {
synchronized (MyLock.right) {
System.out.println("Girl拥有右筷子,等待左筷子");
synchronized (MyLock.left) {
System.out.println("Girl可以吃饭");
}
}
}
}
各线程之间访问资源保持顺序性, 如上的男孩女孩获取筷子的示例,我们只需要让男孩线程和女孩线程获取筷子的顺序保持一致,就可以避免死锁。
效率问题
synchronized锁升级
synchronized的使用通常有三种形式
关于临界区。所谓“临界区”,指的是某一块代码区域,它同一时刻只能由一个线程执行。在上面的例子中,如果synchronized关键字在方法上,那临界区就是整个方法内部。而如果是使用synchronized代码块,那临界区就指的是代码块内部的区域。
锁的状态总共有四种
- 无锁:当一个线程第一次访问一个对象的同步块时,JVM会在对象头中设置该线程的Thread ID,并将对象头的状态位设置为“偏向锁”。这个过程称为“偏向”,表示对象当前偏向于第一个访问它的线程。
- 偏向锁:偏向锁是指在只有一个线程访问对象的情况下,该线程不需要使用同步操作就可以访问对象。这种情况下,JVM 会在对象头中记录该线程的ID作为偏向锁的持有者,并将对象头中的Mark Word中的一部分作为偏向锁标识。在这种情况下,如果其他线程访问该对象,会先检查该对象的偏向锁标识,如果和自己的线程ID 相同,则直接获取锁。如果不同,则该对象的锁状态就会升级到轻量级锁状态。
- 轻量级锁:当一个线程访问该对象时,JVM会将对象头中的Mark Word复制一份到线程栈中,并在对象头中存储线程栈中的指针。此时,如果另一个线程想要访问该对象,会发现该对象已经处于轻量级锁状态,并尝试使用CAS操作将线程栈中的指针替换成自己的指针。此时锁升级为重量级锁。
- 重量级锁:如果一个线程想要获取该对象的锁,则需要先进入等待队列,等待该锁被释放。当锁被释放时,JVM 会从等待队列中选择一个线程唤醒,并将该线程的状态设置为“就绪”状态,然后等待该线程重新获取该对象的锁。
自旋锁
因为挂起线程以及恢复线程要转移到操作系统内核模式执行,这会给性能带来极大的影响。
在JDK定义中,自旋锁默认的自旋次数为10次,用户可以使用参数-XX:PreBlockSpin来更改。
自适应锁
在JDK 1.6中引入了。获取锁的自旋次数不确定,根据之前的数据来确认自旋次数或者不自旋,让JVM变得更聪明。
消除锁
锁消除是指虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然就无须再进行。变量是否逃逸,对于虚拟机来说是需要使用复杂的过程间分析才能确定的,但是程序员自己应该是很清楚的,怎么会在明知道不存在数据争用的情况下还要求同步呢?
这个问题的答案是:有许多同步措施并不是程序员自己加入的,同步的代码在Java程序中出现的频繁程度也许超过了大部分人的想象。我们来看看如下例子,这段非常简单的代码仅仅是输出三个字符串相加的结果,无论是源代码字面上,还是程序语义上都没有进行同步。
public String concatString(String s1, String s2, String s3) {
return s1 + s2 + s3;
}
String是一个不可变的类,对字符串的连接操作总是通过生成新的String对象来进行的,因此Javac编译器会对String连接做自动优化。在JDK 5之前,字符串加法会转化为StringBuffer对象的连续append()操作,在JDK 5及以后的版本中,会转化为StringBuilder对象的连续append()操作,以上代码会变成如下代码。
public String concatString(String s1, String s2, String s3) {
StringBuffer sb = new StringBuffer()
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
每个StringBuffer.append()方法中都有一个同步块,锁就是sb对象 。 虚拟机观察变量sb ,经过逃逸分析后会发现它的动态作用域被限制在concatString( ) 方 法内部 。 也就是sb的 所有引用都永远不会逃逸到 concatString( )方法之外 , 其他线程无法访问到它 , 所以这里虽然有锁,但是可以被安全地消除掉。在解释执行时这里仍然会加锁,但在经过服务端编译器的即时编译之后,这段代码就会忽略所有的同步措施而直接执行。
锁粗化
原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小——只在共享数据 的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能变少,即使存在锁竞争,等待锁的线程也能尽可能快地拿到锁。 大多数情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体之中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。如上代码连续的append()方法就属于这类情况。如果虚拟机探测到有这样一串零碎的操作 都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部,扩展到第一个append()操作之前直至最后一个append()操作之后,这样只需要加锁一次就可 以了。
使用Synchronized需要注意的点
- 锁对象不能为空,因为锁的信息都保存在对象头里。
- 作用域不宜过大,影响程序执行的效率。
- 避免死锁(多个线程获取锁的顺保持一致)
- 在能选择的情况下,既不要用Lock也不要用synchronized关键字,用java.util.concurrent包中的各种各样的类,如果不用该包下的类,在满足业务的情况下,可以使用synchronized关键字,因为代码量少,避免出错。
synchronized是公平锁吗?
synchronized实际上是非公平的,新来的线程有可能立即获得监视器,而在等待区中等候已久的线程可能再次等待,这样有利于提高性能,但是也可能会导致饥饿现象。
3. Lock
在jdk1.5之前实现线程同步只能使用synchronized和volatile两个关键字。因为synchronized是关键字,所以在使用上受限,使用synchronized实现同步,那么在等待过程中的其他线程会一直等待下去,没有机制打断他们,这很容易产生死锁。另外,synchronized是jvm支持,相对来说锁比较重。volatile关键字只能保证可见性和有序顺(禁止指令重排序从而保证顺序性),但是它不能保证原子性。保证原子性需要使用synchronized。
Lock锁
ReentrantLock:Lock接口的实现类,与synchronized一样具有互斥锁功能。
Lock lock = new ReentrantLock();
lock.lock;//加锁
try {
//业务逻辑
} finally {
lock.unlock();//释放锁
}
读写锁
ReentrantReadWriteLock:一种支持一写多读的同步锁,读写分离,可分别分配读锁、写锁。支持多次分配读锁,使多个读操作可以并发执行。
互斥规则:
- 写-写:互斥,阻塞。
- 读-写:互斥,读阻塞写、写阻塞读。
- 读-读:不互斥、不阻塞。
- 在读操作远远高于写操作的环境中,可在保障线程安全的情况下,提高运行效率。
重入锁
重入锁:一个线程获取到一个对象的锁之后还可以继续再次获得该锁,对象头有一个专门记录获得锁次数的标记。
- 重入锁也叫作递归锁,指的是同一个线程外层方法获取到一把锁后,内层方法同样具有这把锁的控制权限。
- synchronized和Lock锁都可以实现锁的重入。
公平锁
根据线程获取锁的抢占机制,锁可以分为公平锁和非公平锁,公平锁表示线程获取锁 的顺序是按照线程请求锁的时间早晚来决定的,也就是最早请求锁的线程将最早获取到锁。 而非公平锁则在运行时闯入,也就是先来不一定先得 。
ReentrantLock 提供了公平和非公平锁的实现 。
//公平锁:
ReentrantLock pairLock =new ReentrantLock(true)。
//非公平锁:
ReentrantLock NonPairLock=new ReentrantLock(false)。
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
如果构造函数不传递参数,则默认是非公平锁 。
例如,假设线程 A 已经持有了锁,这时候线程 B 请求该锁其将会被挂起。 当线程 A 释放锁后,假如当前有线程 C 也需要获取该锁,如果采用非公平锁方式,则根据线程调度策略, 线程 B 和线程 C 两者之一可能获取锁,这时候不需要任何其他干涉,而如果使用 公平锁则需要把C挂起,让B获取当前锁 。
在没有公平性需求的前提下尽量使用非公平锁,因为公平锁会带来性能开销 。
synchronized的缺陷
效率低:锁的释放情况少,只有代码执行完毕或者异常结束会释放锁;试图获取锁的时候不能设定超时,不能中断一个正在使用锁的线程,相对而言,Lock可以中断和设置超时。
不够灵活:加锁和释放的时机单一,每个锁仅有一个单一的条件(某个对象),相对而言,读写锁更加灵活。
无法知道是否成功获得锁,相对而言,Lock可以拿到状态,如果成功获取锁,则返回true....,如果获取失败,返回false。
Lock解决synchronized的缺陷
Lock类有以下4个方法:
- lock(): 加锁
- unlock(): 解锁
- tryLock(): 尝试获取锁,返回一个boolean值
- tryLock(long,TimeUtil): 尝试获取锁,可以设置超时
tryLock(),演示一个线程拿到锁后,另一个线程再次调用tryLock()就会返回false
public class TryLockDemo {
//创建锁
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
//创建线程去调用m方法
for (int i = 0; i < 2; i++) {
new Thread(new Runnable() {
@Override
public void run() {
m();
}
}).start();
}
}
public static void m() {
if (lock.tryLock()) {
System.out.println(Thread.currentThread().getName() + "拿到锁");
try {
Thread.sleep(5000);
System.out.println(Thread.currentThread().getName() + "醒了");
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println(Thread.currentThread().getName() + "获取锁失败 : " + lock.tryLock());
}
}
}
tryLock(time, TimeUtil)
synchronized锁只与一个条件(是否获取锁)相关联,多线程竞争一个锁时,其余未得到锁的线程只能不停的尝试获得锁,而不能中断。高并发的情况下会导致性能下降。ReentrantLock的lockInterruptibly()方法可以优先考虑响应中断。 一个线程等待时间过长,它可以中断自己,然后ReentrantLock响应这个中断,不再让这个线程继续等待。有了这个机制,使用ReentrantLock时就不会像synchronized那样产生死锁了。
Synchronized和ReentrantLock
除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。
4. 线程通信【生产者-消费者】
线程通信:线程之间可以通过共享内存的方式通信。
若干个生产者在生产产品,这些产品将提供给若干个消费者去消费,为了使生产者和消费者能并发执行,在两者之间设置一个能存储多个产品的缓冲区,生产者将生产的产品放入缓冲区中,消费者从缓冲区中取走产品进行消费,显然生产者和消费者之间必须保持同步,即不允许消费者到一个空的缓冲区中取产品,也不允许生产者向一个满的缓冲区中放入产品。
实现线程通信的条件:
- 调用wait()和notify()必须拥有相同的对象锁
- 方法必须在synchronized方法或代码块中
- wait() 调用该对象的某个线程会阻塞挂起
- notify() 调用该对象的notify方法会唤醒在该对象上调用wait等待中的线程。具体是哪个线程,是随机的。
- notifyAll()
Sleep和wait的区别:sleep会抱着锁睡,wait进入等待时会释放自己持有的锁。
5. 线程的生命周期
6.线程池
概念
- 如果有非常的多的任务需要多线程来完成,且每个线程执行时间不会太长,这样频繁的创建和销毁线程。
- 频繁创建和销毁线程会比较耗性能。有了线程池就不要创建更多的线程来完成任务,因为线程可以重用
- 线程池用维护者一个队列,队列中保存着处于等待(空闲)状态的线程。不用每次都创建新的线程。
线程池实现逻辑
线程池的使用
常用的线程池接口和类(所在包java.util.concurrent)。
Executor:线程池的顶级接口。
ExecutorService:线程池接口,可通过submit(Runnable task)提交任务代码。
Executors工厂类:通过此类可以获得一个线程池。
方法名 | 描述 |
---|---|
newFixedThreadPool(int nThreads) | 获取固定数量的线程池。参数:指定线程池中线程的数量。 |
newCachedThreadPool() | 获得动态数量的线程池,如不够则创建新的。 |
Callable接口
JDK1.5加入,与Runnable接口类似,实现之后代表一个线程任务。
Callable具有泛型返回值、可以声明异常。
public interface Callable< V >{ public V call() throws Exception; }
public class TestCallable1 {
public static void main(String[] args) {
//1、创建线程池对象
ThreadPoolExecutor es = new ThreadPoolExecutor(3, 5, 0L,
TimeUnit.MICROSECONDS, new ArrayBlockingQueue<>(1024));
//2、通过线程池提交线程并执行任务
es.submit(new MyCallable());
//3、关闭线程池
es.shutdown();
}
}
class MyCallable implements Callable{
@Override
public Object call() throws Exception {
for (int i = 0; i < 10; i++) {
Thread.sleep(10);
System.out.println(Thread.currentThread().getName()+"--->"+i);
}
return null;
}
}
Future
接口
Future接口表示将要执行完任务的结果。
get()以阻塞形式等待Future中的异步处理结果(call()的返回值)。
//1+...+100
import java.util.concurrent.*;
public class CallableTest {
public static void main(String[] args) {
//创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(2);
//提交线程任务
Future<Integer> future = executorService.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < 51; i++) {
sum += i;
}
return sum;
}
});
Future<Integer> future1 = executorService.submit(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 51; i <101 ; i++) {
sum += i;
}
return sum;
}
});
//输出两个线程返回的结果
try {
System.out.println(future.get() + future1.get());
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
//选择不关闭线程池
}
}