线程使用
1、实现 Runnable 接口;
2、实现 Callable 接口;
3、继承 Thread 类。
4、实现 Runnable 和 Callable
1、实现Runnable接口
public class MyRunnable implements Runnable {
@Override
public void run() {
// ...
}
}
public static void main(String[] args) {
MyRunnable myRunnable= new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
}
2、实现 Callable 接口
public class MyCallable implements Callable<Integer> {
public String call() {
return "XXXXX";
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
MyCallable myCallable= new MyCallable();
FutureTask<Integer> ft = new FutureTask<>(myCallable);
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}
3、继承 Thread 类
public class MyThread extends Thread {
public void run() {
// ...
}
}
public static void main(String[] args) {
MyThread myThread= new MyThread();
myThread.start();
}
接口VS继承
因为接口可以继承多个,而继承只能继承一个类 所以通常需要使用线程的时候用继承接口的方法
线程的五种状态
1、新建状态(new)
创建线程 Thread t= new Thread()
2、就绪状态(Runnable)
调用线程的start方法 线程到达就绪状态等待OS系统调用 t.start()
3、运行状态(Running)
操作系统调用此线程 此线程到达运行状态
4、阻塞状态(Block)
(1)等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中,
进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒,
(2)同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中。
(3)其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。
当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5、结束状态(Dead)
线程流程图
sleep()与wait()的区别
1、sleep()Thread类中的方法,wait()是Object类的方法。
2、sleep()不释放对象锁,wait()放弃对象锁。
3、wait()和sleep()都是让出CPU占有权,让其它线程能够得到运行,不同的地方在于wait()可以通过notify()或者notifyAll()主动唤醒或者wait一定的等待时间自动恢复运行,而sleep方法只能在等待一定的时间后自动恢复运行。
4、sleep()可以不在synchronized的块下调用,sleep()不会丢失当前线程对任何对象的同步锁(monitor); wait()必须在synchronized的块下来使用,调用了之后失去对object的monitor, 这样做的好处是它不影响其它的线程对object进行操作。
5、wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态。
6、wait()进入等待锁定池,notify()、notifyAll()方法可以使其获得对象锁,进入就绪状态。
notify()与notifyAll()
唤醒等待池队列的线程进入到锁池队列参与CPU时间片的竞争
互斥同步
Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问,第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock。
synchronized
同步一个静态方法
会对此类进行加锁 不同线程对同一类对象的静态方法,静态属性的访问需要进行同步
同步一个成员变量
会对同一对象的属性方法进行同步
同步一个代码块
调用的是同一个对象的同步代码块
ReentrantLock
ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁。
public class LockExample {
private Lock lock = new ReentrantLock();
public void func() {
lock.lock();
try {
for (int i = 0; i < 10; i++) {
System.out.print(i + " ");
}
} finally {
lock.unlock(); // 确保释放锁,从而避免发生死锁。
}
}
}
public static void main(String[] args) {
LockExample lockExample = new LockExample();
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> lockExample.func());
executorService.execute(() -> lockExample.func());
}
比较
锁的实现
synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
性能
新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等,synchronized 与 ReentrantLock 大致相同。
等待可中断
当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情。
ReentrantLock 可中断,而 synchronized 不行。
公平锁
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。
synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但是也可以是公平的
锁绑定多个条件
一个 ReentrantLock 可以同时绑定多个 Condition 对象。
使用选择
除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。
扩展JVM synchronized
jdk1.6版本之前,synchronized 是一个重量级锁,但在JDK1.6之后对synchronized 进行了优化
实现原理
JAVA对象头
首先什么是JOL
JOL即Java Object Layout Java对象布局
我们可以通过一些插件了解对象的组成部分
markword就是常说的java对象头 8个字节
klasspoint指定该对象的class类对象 4个字节
基本变量:用于存放java八种基本类型成员变量,以4byte步长进行补齐,使用内存重排序优化空间
引用变量:存放对象地址,如String,Object;占用4个字节,64位jvm上默认使用-XX:+UseCompressedOops进行压缩,可使用-XX:-UseCompressedOops进行关闭,则在64位jvm上会占用8个字节
补齐:对象大小必须是8byte的整数倍,用来补齐字节数。Object o = new Object() 在内存中占用16个字节,其中最后4个是补齐
这里我们以64位操作系统来举例,上图就是对象头的内容
下面来讲解synchronized 与java对象头的联系
锁一共有四种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态随着竞争情况逐渐升级。为了提高获得锁和释放锁的效率,锁可以升级但不能降级,意味着偏向锁升级为轻量级锁后不能降级为偏向锁。
1,无锁状态
对象在刚创建的时候,没有线程对它进行调用,此时对象处于无锁状态
2,偏向锁状态
对象被一个线程(t1线程)进行调用时,会在对象头和栈帧的锁记录里存储偏向的线程ID(相当于在门上的锁上贴了一个标签,告诉其他线程,目前我这个线程(t1)正在占用),一旦有其他线程(t2,t3,t4…)要调用这个对象,发现其偏向锁的位置为1,就会进行自旋(CAS)操作,CAS将对象头的偏向锁指向本线程(t2,t3,t4…),触发偏向锁的撤销。偏向锁只有在竞争出现才会释放锁。当其他线程(t2,t3,t4…)尝试竞争偏向锁时,程序到达全局安全点后(没有正在执行的代码),它会查看Java对象头中记录的线程是否存活(t1线程),如果没有存活,那么锁对象被重置为无锁状态,其它线程可以竞争将其设置为偏向锁;如果存活,那么立刻查找该线程(t1线程)的栈帧信息,如果还是需要继续持有这个锁对象,那么暂停当前线程,撤销偏向锁,升级为轻量级锁,如果线程(t1线程)不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程(t2,t3,t4…)
3,轻量级锁
轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。
轻量级锁的加锁过程:
1、在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。
2,拷贝对象头中的Mark Word复制到锁记录中;
3,拷贝成功后,虚拟机将在当前线程栈使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤4,否则执行步骤5。
4,如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,这时候线程堆栈与对象头的状态如图所示。
5,如果这个更新操作失败了,虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。
轻量级锁的释放
释放锁线程视角:由轻量锁切换到重量锁,是发生在轻量锁释放锁的期间,之前在获取锁的时候它拷贝了锁对象头的markword,在释放锁的时候如果它发现在它持有锁的期间有其他线程来尝试获取锁了,并且该线程对markword做了修改,两者比对发现不一致,则切换到重量锁。
synchronized的执行过程:
- 检测Mark Word里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁
- 如果不是,则使用CAS将当前线程的ID替换Mard Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1
- 如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。
- 当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁
- 如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
- 如果自旋成功则依然处于轻量级状态。
- 如果自旋失败,则升级为重量级锁。