Java多线程详解

  • 1.进程与线程
  • 2.多线程操作
  • 3.多线程特性
  • 4.线程安全
  • 4.1 synchronized实现线程安全
  • 4.1.1同步处理
  • 4.1.2 synchronized底层实现(对象的Monitor机制):
  • 4.1.3 JDK1.6之后对synchronized的优化:
  • 4.2 Lock实现线程安全
  • 4.2.1Lock使用方式
  • 4.2.2Lock接口的重要方法
  • 4.3 synchronized与Lock的关系和区别


1.进程与线程

进程是OS中资源分配的最小单元。一个进程至少有一个线程,如果该进程只有一个线程,则称为单线程;否则为多线程。

线程是OS中任务分配的最小单元。
线程:守护线程和用户线程
守护线程:在后台运行,只要JVM中存在任意一个用户线程没有终止,守护线程则一直在运行,当JVM中最后一个用户线程终止,守护线程也会随着JVM一同停止。例:GC线程
用户线程:除守护线程之外的都为用户线程,默认创建的都为用户线程;例:主线程

线程优势:创建于销毁一个线程的开销要比一个进程小的很多,线程间通信也比进程间通信容易得多。

2.多线程操作

线程间通信使用的方法join(), wait()/notify(), yield(), sleep()

  • sleep(): 线程:运行 --> 阻塞; 当前线程立即交出CPU,进入阻塞态,不会释放对象锁。
  • yield(): 线程:运行 --> 就绪; 系统调度交出CPU,进入就绪态,不会释放对象锁。
  • join(): 线程:运行 --> 阻塞; 释放对象锁(join内部就是wait())。
  • wait(): 线程:运行 --> 阻塞;释放对象锁, wait()方法必须与 synchronized关键字搭配使用
  • notify():线程:阻塞 --> 就绪;必须在同步方法或同步代码块中使用。
    线程状态之间的转换:
  • java 线程锁的释放 java线程锁的作用_线程

3.多线程特性

多线程三特性:原子性,可见性,有序性

  • 原子性:指一个操作不可中断,即使是多个线程一起执行的时候,一旦操作开始,就不会被其他线程所干扰。
    -例如:两线程对同一变量赋值,那此变量的值有两种情况,但线程之间互不干扰。
  • 可见性:只当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改,即每个线程都能立即得知共享变量的变化。串行操作不存在可见性。
  • 有序性:并发操作可能会出现程序执行乱序:写在前面的代码后执行。原因:程序执行可能会进行指令重排,重排之后指令不一定和原指令一致。

4.线程安全

4.1 synchronized实现线程安全

线程之间抢夺资源导致线程不安全,因此同步处理是实现线程安全的主要方式。

4.1.1同步处理

使用synchronized关键字处理同步有两种方式:同步代码块,同步方法

同步代码块处理

  • synchronized(锁的对象){ }
  • 普通对象(对象锁)
  • 类.class

同步方法处理

  • 成员同步方法 锁的是当前对象 this
  • 静态同步方法 锁的是当前类 .class

4.1.2 synchronized底层实现(对象的Monitor机制):

任意一个对象都有Monitor,synchronized对象锁实际上就是获取该对象的Monitor。
当前对象要想获取到该锁的Monitor流程:

  1. 先判断锁对象Monitor计数器的值是否为0;
  2. 如果为0:表示此时Monitor还未被任何线程所持有,当前线程获取Monitor,并将持有线程置为自己,将Monitor的值+1;
  3. 如果不为0:表示此时Monitor已经被某个线程所持有,然后判断该线程是否为自己(当前线程);
    ① 如果为自己,再将Monitor的值+1;
    ② 如果不为自己,线程进入阻塞态,直到Monitor的值减为0;

可重入锁:即为①情况。持有锁的线程再次获取锁。

4.1.3 JDK1.6之后对synchronized的优化:

将锁分为:偏向锁->轻量级锁->重量级锁(自适应自旋)
偏向锁(默认初始锁,乐观锁,锁一直是一个线程来回获取):
当线程第一次获取锁时,将偏向锁线程置为当前线程,以后再次获取锁时,不再有加锁和解锁过程,只判断获取锁的线程是否为当前线程。

轻量级锁:在不同时间段内有不同的线程尝试获取锁,每次锁的获取都需要加锁和解锁的过程。

重量级锁:在同一时刻有不同线程尝试获取锁。
(JDK1.6之前默认实现:线程获取锁失败直接进入阻塞态,也就是OS从 用户态–>内核态(开销太大))

锁只能从偏向锁升级为轻量级锁,在升级为重量级锁,不能反向降级

锁粗化:将多次连续的加减锁过程粗化为一次大的加减锁过程

private static StringBuffer sb = new StringBuffer();
public static void main(String[] args) {
    {
        sb.append("a");
        sb.append("b");
        sb.append("c");
    }
}

锁消除:在没有多线程访问的场景下,将锁直接消除。

public static void main(String[] args) {
     StringBuffer sb = new StringBuffer();
     sb.append("a");
     sb.append("b");
     sb.append("c");
}

死锁产生:同时满足死锁产生的四个条件

  1. 互斥:共享资源X,Y只能被一个线程占用;
  2. 占有且等待:线程1已经获取共享资源X,并不释放,同时在等待线程2释放共享资源Y;
  3. 不可抢占:其他线程无法抢占线程1持有的资源X;
  4. 循环等待:线程1等待线程2的资源,线程2等待线程1的资源;

死锁现象:程序出现“假死”

死锁解决:破坏任意一个条件

4.2 Lock实现线程安全

Lock体系的产生:死锁条件难以破坏,死锁问题难以解决。
JDK1.5引入Lock体系解决死锁问题

4.2.1Lock使用方式

try{
			lock.lock();
			}catch(Exception e){
			
		}finally {
			lock.unlock();
		}

4.2.2Lock接口的重要方法

lock():获取锁:如果锁不可用,则当前线程将被禁用,以进行线程调度,并处于休眠状态,直到获取锁。

unlock():释放锁:通常只有所的持有者可以释放它,无权限的释放可能会引发(未检查)异常;

1.响应中断:void lockInterruptibly() throws InterruptedException;

2.非阻塞式获取锁,若获取锁失败,线程继续执行,不再阻塞:
(只有在调用时获取锁)
boolean tryLock();

3.支持超时,获取锁失败的线程等待一段时间后若还获取到锁,线程退出:
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

4.Lock的常用子类
ReentrantLock: 可重入锁(Lock接口中常用的子类,语义与synchronized基本一致,也是独占锁的实现)

小补充:
同步队列:所有获取锁失败的线程进入同步队列排队获取锁。
等待队列:调用wait的线程置入等待队列,等待被唤醒(notify)。
synchronized不公平;Lock可以实现公平锁;

4.3 synchronized与Lock的关系和区别

1.关系:synchronized与ReentrantLock都属于独占锁的实现,都支持可重入
2.区别:

  • synchronized是关键字,JVM层面的实现
    ReentrantLock是java语言层面的实现
  • ReentrantLock具备一些synchronized不具备的特性,如:相应中断,支持超时,支持非阻塞式获取锁,可以实现公平锁(默认非公平锁),可以实现读写锁。
  • synchronized只有一个等待队列,而Lock调用newCondition()产生多个等待队列。