JavaSE-多线程(二)
线程控制
在 java 中可以开启线程吗?
不行。java操作不了硬件,底层代码是通过c++操作硬件的。
public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
// 调用原生方法
private native void start0(
wait() 和sleep() 区别?
- 来自不同的类
wait => Object
sleep => Thread
- 关于锁的释放
wait => 会释放锁
sleep => 不会释放锁
- 使用范围不同
wait 必须使用在同步代码块中
sleep 可以使用在任何地方
- 是否需要捕获异常
wait 不用捕获异常
sleep 需要捕获异常
get/setPriority() 优先级别设置
线程默认的优先级别为5
主线程:
public class TestThread2 {
public static void main(String[] args) {
Thread.currentThread().setName("this is main thread");
CreateThread_m_2 createThread_m_2 = new CreateThread_m_2();
Thread thread = new Thread(createThread_m_2, "this is child thread");
thread.start();
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "---------:" + i + "优先级为:" + Thread.currentThread().getPriority());
}
}
}
其他线程:
public class CreateThread_m_2 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "---------:" + i + "优先级为:" + Thread.currentThread().getPriority());
}
}
}
输出结果:优先级为:5(默认)
查看源码发现:
同优先级别的线程采取的策略是先到先得(时间片)策略,如果优先级别高被CPU 调度的概率就高。
使用 setPriority(int) 设置优先级别。
join() 插入线程
join 主线程中插入线程
注意必须先启动线程。再进行插入。
主线程:
class Thread_Runnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "---------:" + i + "优先级为:" + Thread.currentThread().getPriority());
}
}
}
public class Demo {
public static void main(String[] args) throws InterruptedException {
Thread.currentThread().setName("this is main thread");
for (int i = 0; i < 10; i++) {
if (i == 6) {
Thread_Runnable thread_runnable = new Thread_Runnable();
Thread other_thread = new Thread(thread_runnable, "this is other thread");
other_thread.start();
other_thread.join();
}
System.out.println(Thread.currentThread().getName() + "---------:" + i + "优先级为:" + Thread.currentThread().getPriority());
}
}
}
输出结果:
this is main thread---------:0优先级为:5
this is main thread---------:1优先级为:5
this is main thread---------:2优先级为:5
this is main thread---------:3优先级为:5
this is main thread---------:4优先级为:5
this is main thread---------:5优先级为:5
this is other thread---------:0优先级为:5
this is other thread---------:1优先级为:5
this is other thread---------:2优先级为:5
this is other thread---------:3优先级为:5
this is other thread---------:4优先级为:5
this is other thread---------:5优先级为:5
this is other thread---------:6优先级为:5
this is other thread---------:7优先级为:5
this is other thread---------:8优先级为:5
this is other thread---------:9优先级为:5
this is main thread---------:6优先级为:5
this is main thread---------:7优先级为:5
this is main thread---------:8优先级为:5
this is main thread---------:9优先级为:5
sleep() 阻塞线程
使用类调用sleep方法阻塞线程
意思是程序睡眠(…)ms,先执行其他线程的任务,执行完后在执行睡醒的线程。睡眠过程中线程状态处于阻塞状态,睡眠时间结束后线程处于就绪状态等待被CPU 调度。
主线程:
public class TestThread2 {
public static void main(String[] args) throws InterruptedException {
Thread.currentThread().setName("this is main thread");
CreateThread_m_2 createThread_m_2 = new CreateThread_m_2();
// 关联线程
Thread thread = new Thread(createThread_m_2, "this is child thread");
thread.start();
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "---------:" + i + "优先级为:" + Thread.currentThread().getPriority());
}
}
}
子线程:
public class CreateThread_m_2 implements Runnable {
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "---------:" + i + "优先级为:" + Thread.currentThread().getPriority());
}
}
}
实现时钟效果:
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Test {
public static void main(String[] args) throws InterruptedException {
DateFormat df = new SimpleDateFormat("HH:mm:ss");
while (true) {
String str = df.format(new Date());
System.out.println(str);
Thread.sleep(1000);
}
}
}
龟兔赛跑
class Race implements Runnable {
private static String winner;
@Override
public void run() {
for (int i = 0; i <= 100; i++) {
if (Thread.currentThread().getName().equals("兔子") && i % 10 == 0) {
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (gameOver(i)) break;
System.out.println(Thread.currentThread().getName() + "--->跑了" + i + "步");
}
}
private boolean gameOver(int i) {
if (winner != null) {
return true;
}
{
if (i == 100) {
winner = Thread.currentThread().getName();
System.out.println("比赛结束胜利者是:" + winner);
return true;
}
}
return false;
}
}
public class Demo {
public static void main(String[] args) {
Race race = new Race();
new Thread(race, "兔子").start();
new Thread(race, "乌龟").start();
}
}
输出:
乌龟--->跑了0步
乌龟--->跑了1步
乌龟--->跑了2步
乌龟--->跑了3步
乌龟--->跑了4步
乌龟--->跑了5步
乌龟--->跑了6步
乌龟--->跑了7步
乌龟--->跑了8步
乌龟--->跑了9步
乌龟--->跑了10步
乌龟--->跑了11步
乌龟--->跑了12步
乌龟--->跑了13步
乌龟--->跑了14步
乌龟--->跑了15步
乌龟--->跑了16步
乌龟--->跑了17步
乌龟--->跑了18步
乌龟--->跑了19步
乌龟--->跑了20步
乌龟--->跑了21步
乌龟--->跑了22步
乌龟--->跑了23步
乌龟--->跑了24步
乌龟--->跑了25步
乌龟--->跑了26步
乌龟--->跑了27步
乌龟--->跑了28步
乌龟--->跑了29步
乌龟--->跑了30步
乌龟--->跑了31步
乌龟--->跑了32步
乌龟--->跑了33步
乌龟--->跑了34步
乌龟--->跑了35步
乌龟--->跑了36步
乌龟--->跑了37步
乌龟--->跑了38步
乌龟--->跑了39步
乌龟--->跑了40步
乌龟--->跑了41步
乌龟--->跑了42步
乌龟--->跑了43步
乌龟--->跑了44步
乌龟--->跑了45步
乌龟--->跑了46步
乌龟--->跑了47步
兔子--->跑了0步
乌龟--->跑了48步
兔子--->跑了1步
兔子--->跑了2步
乌龟--->跑了49步
兔子--->跑了3步
乌龟--->跑了50步
兔子--->跑了4步
乌龟--->跑了51步
兔子--->跑了5步
乌龟--->跑了52步
兔子--->跑了6步
乌龟--->跑了53步
兔子--->跑了7步
乌龟--->跑了54步
乌龟--->跑了55步
乌龟--->跑了56步
乌龟--->跑了57步
乌龟--->跑了58步
乌龟--->跑了59步
乌龟--->跑了60步
乌龟--->跑了61步
乌龟--->跑了62步
乌龟--->跑了63步
乌龟--->跑了64步
乌龟--->跑了65步
乌龟--->跑了66步
乌龟--->跑了67步
乌龟--->跑了68步
兔子--->跑了8步
乌龟--->跑了69步
兔子--->跑了9步
乌龟--->跑了70步
乌龟--->跑了71步
乌龟--->跑了72步
乌龟--->跑了73步
乌龟--->跑了74步
乌龟--->跑了75步
乌龟--->跑了76步
乌龟--->跑了77步
乌龟--->跑了78步
乌龟--->跑了79步
乌龟--->跑了80步
乌龟--->跑了81步
乌龟--->跑了82步
乌龟--->跑了83步
乌龟--->跑了84步
乌龟--->跑了85步
乌龟--->跑了86步
乌龟--->跑了87步
乌龟--->跑了88步
乌龟--->跑了89步
乌龟--->跑了90步
乌龟--->跑了91步
乌龟--->跑了92步
乌龟--->跑了93步
乌龟--->跑了94步
乌龟--->跑了95步
乌龟--->跑了96步
乌龟--->跑了97步
乌龟--->跑了98步
乌龟--->跑了99步
比赛结束胜利者是:乌龟
yield() 线程就绪(唤醒)
调用yield 方法实现将线程转换到就绪状态,等待CPU 的调度(阻塞)
public class Demo extends Thread {
public Demo(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.getName());
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Demo("线程" + i).start();
}
// main gc
while (Thread.activeCount() > 2) {
Thread.currentThread().yield();
}
System.out.println("continue");
}
}
输出结果:
线程0
线程1
线程2
线程3
线程4
线程5
线程6
线程7
线程8
线程9
continue
setDaemon() 伴随线程
设置伴随线程
子线程,伴随着主线程执行完任务后一起消亡,子线程可以垂死挣扎下。
注意:一定要设置在启动线程前。
public class Test extends Thread {
public Test(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(this.getName() + "----" + i);
}
}
public static void main(String[] args) {
Thread.currentThread().setName("主线程");
Test t1 = new Test("子线程");
t1.setDaemon(true);
t1.start();
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "------:" + i);
}
}
}
主线程------:0
主线程------:1
主线程------:2
主线程------:3
主线程------:4
主线程------:5
主线程------:6
主线程------:7
主线程------:8
主线程------:9
子线程----0
子线程----1
子线程----2
子线程----3
stop() 停止线程
杀死线程
public class Test {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
if (i == 6) {
Thread.currentThread().stop();
}
System.out.println(i);
}
}
}
0
1
2
3
4
5
多线程如何安全?
多线程会存在一个问题就是线程与线程间的调度问题(线程的安全问题),这个问题会导致一个线程还没执行完任务就被下一个线程就开始执行了,往往就会导致错误结果的发生,例如创建线程方式一中的火车票问题。
如果你的代码在多线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的
所以解决方法就是加锁(同步监听)
加锁的两种方式
- 同步代码块
主线程:
public class Lock {
public static void main(String[] args) {
Test test = new Test();
new Thread(test, "窗口1").start();
new Thread(test, "窗口2").start();
new Thread(test, "窗口3").start();
}
}
子线程:
public class Test implements Runnable {
private static int num = 10;
@Override
public void run() {
for (int i = 0; i < 100; i++) {
synchronized (this){// ()中传入的是this,实际上就是该类的实例化对象
if (num > 0) {
System.out.println(Thread.currentThread().getName() + "卖了张票,剩余:" + (--num) + "张");
}
}
}
}
}
- 同步方法
子线程:
class Test implements Runnable {
private int num = 10;
@Override
public void run() {
reduceNum();
}
private synchronized void reduceNum() {
for (int i = 0; i < 100; i++) {
if (num > 0) System.out.println(Thread.currentThread().getName() + "卖了张票,剩余:" + (--num) + "张");
}
}
}
public class Demo {
public static void main(String[] args) {
Test test = new Test();
new Thread(test, "窗口1").start();
new Thread(test, "窗口2").start();
new Thread(test, "窗口3").start();
}
}
过程分析
比如说线程1 先被CPU 调度进去,看到程序中有锁但是没有锁住,然后它将这个锁锁住。下一线程被CPU 调度进去发现有锁并且锁住了,这时只能等着。等到上一个线程执行完后,锁被释放了,哪个线程抢到哪个线程就上锁,执行,释放…以此类推(类似于上卫生间~)
代码块中的this 代表的是什么?
谁调用run 方法this 就指向谁,也就是说可以调用run 方法的对象有多少个锁就有多少个。
演示:
主线程:
public class Lock2 {
public static void main(String[] args) {
new Test2("窗口1").start();
new Test2("窗口2").start();
new Test2("窗口3").start();
}
}
子线程:
public class Test2 extends Thread {
private static int num = 10;
public Test2(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (num > 0) {
synchronized (this) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.getName() + "买了一张票,剩余:" + (--num) + "张");
}
}
}
}
}
这里有一个重要的概念。关键字synchronized取得的锁都是对象锁,而不是把一段代码或方法(函数)当作锁,哪个线程先执行带synchronized关键字的方法,哪个线程就持有该方法所属对象的锁,其他线程都只能呈等待状态。但是这有个前提:既然锁叫做对象锁,那么势必和对象相关,所以多个线程访问的必须是同一个对象。
窗口3买了一张票,剩余:9张
窗口2买了一张票,剩余:8张
窗口1买了一张票,剩余:7张
窗口1买了一张票,剩余:6张
窗口2买了一张票,剩余:5张
窗口3买了一张票,剩余:4张
窗口1买了一张票,剩余:3张
窗口3买了一张票,剩余:2张
窗口2买了一张票,剩余:3张
窗口1买了一张票,剩余:1张
窗口3买了一张票,剩余:1张
窗口2买了一张票,剩余:1张
窗口2买了一张票,剩余:0张
窗口1买了一张票,剩余:-1张
窗口3买了一张票,剩余:0张
解决方法:锁必须唯一(共用) !!!
public class Test2 extends Thread {
private static int num = 10;
public Test2(String name) {
super(name);
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
synchronized (Test2.class) {
if (num > 0) {
System.out.println(this.getName() + "买了一张票,剩余:" + (--num) + "张");
}
}
}
}
}
注意:synchronized () 中的参数只能是引用类型的参数
锁问题可以和现实的上卫生间问题类比下,资源(茅坑),锁(门锁),人(线程)
线程加锁与不加锁的优缺点
加锁,线程安全,效率低,可能存在死锁问题。不加锁,线程不安全,效率高。
线程死锁:
public class DeadLock implements Runnable {
public int flag = 1;
static Object o1 = new Object(), o2 = new Object();
@Override
public void run() {
System.out.println(this.flag);
if(this.flag==0){
// 如果flag 为 0 就锁住o1
synchronized (o1){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
synchronized(o2){
System.out.println(Thread.currentThread().getName()+"o2");
}
}
}else {
// 如果flag 为 1 就锁住o2
synchronized (o2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
synchronized(o1){
System.out.println(Thread.currentThread().getName()+"o1");
}
}
}
}
public static void main(String[] args) {
// 创建两个线程
DeadLock deadLock = new DeadLock();
deadLock.flag = 0;
DeadLock deadLock2 = new DeadLock();
deadLock2.flag = 1;
// 启动线程
new Thread(deadLock, "线程1").start();
new Thread(deadLock2, "线程2").start();
}
}
分析:启动线程后,线程调度如果是flag 为 0 的线程先就先锁住线程(o1)并经历睡眠等待重新调度,flag 为 1 的线程争抢资源就锁住线程(o2)并经历睡眠等待重新调度。flag 为 0 的线程(o1)重新被CPU 调度输出线程名称发现线程(o2)锁住不执行其中的内容无法执行完任务,也就表明o1线程的锁无法解锁,o1线程只能等待下次调度。flag 为 1 的线程(o2)重新被CPU 调度输出线程名称发现线程(o1)锁住不执行其中的内容无法执行完任务,也就表明o2线程的锁无法解锁,o2线程只能等待下次调度。因此造成了两条线程互相死锁问题。
疑问:
(1)如果static 去掉了还会存在死锁问题吗?不会,因为加了static 关键字表示这是一个静态资源随着类一起加载,不管存在多少个对象都自有o1,o2。如果去掉了static 各自有各自的o1 o2就不会造成死锁问题。
(2)解决方法?不要嵌套对象锁
public class DeadLock implements Runnable {
public int flag = 1;
static Object o1 = new Object(), o2 = new Object();
@Override
public void run() {
System.out.println(this.flag);
if(this.flag==0){
// 如果flag 为 0 就锁住o1
synchronized (o1){
try {
Thread.sleep(1000); // 注意这里一定要有"Thread.sleep(1000)"让线程睡一觉,不然一个线程运行了,另一个线程还没有运行,先运行的线程很有可能就已经连续获得两个锁了。
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
synchronized(o2){
System.out.println(Thread.currentThread().getName()+"o2");
}
}else {
// 如果flag 为 1 就锁住o2
synchronized (o2){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName());
}
synchronized(o1){
System.out.println(Thread.currentThread().getName()+"o1");
}
}
}
public static void main(String[] args) {
// 创建两个线程
DeadLock deadLock = new DeadLock();
deadLock.flag = 0;
DeadLock deadLock2 = new DeadLock();
deadLock2.flag = 1;
// 启动线程
new Thread(deadLock, "线程1").start();
new Thread(deadLock2, "线程2").start();
}
}
造成死锁的四个必要条件:
-
互斥条件:一个资源每次只能被一个进程使用(上述的static资源)
-
请求与保持条件:一个进程因请求资源而阻塞时,对已获的资源保持不放。(上述的synchronized重叠)
-
不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺。(上述的synchronized相互锁)
-
循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。(上述的synchronized相互锁)
上面列出的死锁的四个必要条件,我们只要想办法破环其中一个条件就可以解决死锁问题。
虚拟机级别加锁方式一:synchronized(排它锁)
main
public class Lock {
public static void main(String[] args) {
Test test = new Test();
new Thread(test, "窗口1").start();
new Thread(test, "窗口2").start();
new Thread(test, "窗口3").start();
}
}
Thread
public class Test implements Runnable {
private int num = 10;
@Override
public void run() {
// for (int i = 0; i < 100; i++) {
// // 同步代码块
// synchronized (Test.class){
// if (num > 0) {
// System.out.println(Thread.currentThread().getName() + "卖了张票,剩余:" + (--num) + "张");
// }
// }
// }
for (int i = 0; i < 100; i++) {
reduceNum();
}
}
// 同步方法
private synchronized void reduceNum() {
if (num > 0) {
System.out.println(Thread.currentThread().getName() + "卖了张票,剩余:" + (--num) + "张");
}
}
}
lambda 表达式
main
public class Demo {
public static void main(String[] args) {
Test test = new Test();
for (int i = 0; i < 100; i++) {
// new Thread(new Runnable() {
// @Override
// public void run() {
// test.run();
// }
// }).start();
// 为什么能使用lambda 表达式? 因为:Runnable 是一个函数式接口.
new Thread(() -> {
test.run();
}).start();
}
}
}
Thread
class Test{
private int num = 10;
public void run() {
synchronized (Test.class) {
if (num > 0) {
System.out.println(Thread.currentThread().getName() + "卖了张票,剩余:" + (--num) + "张");
}
}
}
}
synchronized锁重入
关键字synchronized拥有锁重入的功能。
所谓锁重入的意思就是:当一个线程得到一个对象锁后,再次请求此对象锁时可以再次得到该对象的锁。
看一个例子:
public class ThreadDomain16
{
public synchronized void print1()
{
System.out.println("ThreadDomain16.print1()");
print2();
}
public synchronized void print2()
{
System.out.println("ThreadDomain16.print2()");
print3();
}
public synchronized void print3()
{
System.out.println("ThreadDomain16.print3()");
}
}
public class MyThread16 extends Thread
{
public void run()
{
ThreadDomain16 td = new ThreadDomain16();
td.print1();
}
}
public static void main(String[] args)
{
MyThread16 mt = new MyThread16();
mt.start();
}
结果:
ThreadDomain16.print1()
ThreadDomain16.print2()
ThreadDomain16.print3()
synchronized 底层、锁升级
同步代码块底层
public class Demo{
public static void main(String[] args) {
method();
}
public static synchronized void method(){
synchronized (Demo.class){
}
}
}
反编译:
javac -encoding UTF-8 Demo.java
javap -p -c -v Demo.class
Classfile /C:/Users/Jming_er/Desktop/javaStudy/src/com/study/Demo.class
Last modified 2022-1-14; size 439 bytes
MD5 checksum 48048095a6758e74309bfddb693dadb0
Compiled from "Demo.java"
public class src.com.study.Demo
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#17 // java/lang/Object."<init>":()V
#2 = Methodref #3.#18 // src/com/study/Demo.method:()V
#3 = Class #19 // src/com/study/Demo
#4 = Class #20 // java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Utf8 Code
#8 = Utf8 LineNumberTable
#9 = Utf8 main
#10 = Utf8 ([Ljava/lang/String;)V
#11 = Utf8 method
#12 = Utf8 StackMapTable
#13 = Class #20 // java/lang/Object
#14 = Class #21 // java/lang/Throwable
#15 = Utf8 SourceFile
#16 = Utf8 Demo.java
#17 = NameAndType #5:#6 // "<init>":()V
#18 = NameAndType #11:#6 // method:()V
#19 = Utf8 src/com/study/Demo
#20 = Utf8 java/lang/Object
#21 = Utf8 java/lang/Throwable
{
public src.com.study.Demo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 2: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: invokestatic #2 // Method method:()V
3: return
LineNumberTable:
line 4: 0
line 5: 3
public static synchronized void method();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=2, args_size=0
0: ldc #3 // class src/com/study/Demo
2: dup
3: astore_0
4: monitorenter
5: aload_0
6: monitorexit
7: goto 15
10: astore_1
11: aload_0
12: monitorexit
13: aload_1
14: athrow
15: return
Exception table:
from to target type
5 7 10 any
10 13 10 any
LineNumberTable:
line 7: 0
line 9: 5
line 10: 15
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 10
locals = [ class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
}
SourceFile: "Demo.java"
synchronized是基于进入和退出管程(Monitor)对象实现(monitorenter和monitorexit), monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块结束的位置,任何一个对象都有一个Monitor与之相关联,当一个线程持有Minitor后,它将处于锁定状态。
对象在内存中的表现:见【面向对象编程(oop)】-【类与实例化对象】
-
同步方法: flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED 执行同步方法的时候,一旦执行到这个方法,就会先判断是否有标志位,然后,ACC_SYNCHRONIZED会去隐式调用刚才的两个指令:monitorenter和monitorexit。所以归根究底,还是monitor对象的争夺。
-
同步代码块:当我们进入一个方法的时候,执行monitorenter,就会获取当前对象的一个所有权,这个时候monitor进入数为1,当前的这个线程就是这个monitor的owner。如果你已经是这个monitor的owner了,你再次进入,就会把进入数+1。同理,当他执行完monitorexit,对应的进入数就-1,直到为0,才可以被其他线程持有。所有的互斥,其实在这里,就是看你能否获得monitor的所有权,一旦你成为owner就是获得者。
从C++源码看synchronized
- C++源码中监视器锁(Monitor)的数据结构
oopDesc–继承–>markOopDesc–方法monitor()–>ObjectMonitor–>enter、exit 获取、释放锁
- ObjectMonitor类
在HotSpot虚拟机中,最终采用ObjectMonitor类实现Monitor。
openjdk\hotspot\src\share\vm\runtime\objectMonitor.hpp源码如下:
ObjectMonitor() {
_header = NULL;//markOop对象头
_count = 0;// 线程获取锁的次数。
_waiters = 0,//等待线程数
_recursions = 0;//重入次数
_object = NULL;//监视器锁寄生的对象。锁不是平白出现的,而是寄托存储于对象中。
_owner = NULL;//指向获得ObjectMonitor对象的线程或基础锁,指向持有ObjectMonitor对象的线程地址。
_WaitSet = NULL;//处于wait状态的线程,会被加入到wait set;
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ;//处于等待锁block状态的线程,会被加入到entry set;
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;// _owner is (Thread *) vs SP/BasicLock
_previous_owner_tid = 0;// 监视器前一个拥有者线程的ID
}
每个线程都有两个ObjectMonitor对象列表,分别为free和used列表,如果当前free列表为空,线程将向全局global list请求分配ObjectMonitor。ObjectMonitor对象中有两个队列:_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表
锁升级
synchronized在JDK1.6之前是重量级锁,在JDK1.6以后对synchronized做了优化,增加了无锁,偏向锁,轻量级锁,锁粗化,锁消除,适应性自旋等操作,大大增加了synchronized的效率。
升级过程不可逆
- 无锁
无锁状态其实就是上面讲的乐观锁,这里不再赘述。
- 偏向锁
Java偏向锁(Biased Locking)是指它会偏向于第一个访问锁的线程,如果在运行过程中,只有一个线程访问加锁的资源,不存在多线程竞争的情况,那么线程是不需要重复获取锁的,这种情况下,就会给线程加一个偏向锁。
偏向锁的实现是通过控制对象Mark Word的标志位来实现的,如果当前是可偏向状态,需要进一步判断对象头存储的线程 ID 是否与当前线程 ID 一致,如果一致直接进入。运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。
这个过程是采用了CAS乐观锁操作的,每次同一线程进入,虚拟机就不进行任何同步的操作了,对标志位+1就好了,不同线程过来,CAS会失败,也就意味着获取锁失败。
- 轻量级锁
当线程竞争变得比较激烈时,偏向锁就会升级为轻量级锁,轻量级锁认为虽然竞争是存在的,但是理想情况下竞争的程度很低,通过自旋方式等待上一个线程释放锁。
目的:在多线程的竞争下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗
流程:
1. 判断当前对象是否处于无锁状态(hashcode、0、01),若是,则JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀,即Displaced Mark Word);否则执行步骤(3);
2. JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指正,如果成功表示竞争到锁,则将锁标志位变成00(表示此对象处于轻量级锁状态),执行同步操作;如果失败则执行步骤(3);
3. 判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态;
- 重量级锁
如果线程并发进一步加剧,线程的自旋超过了一定次数,或者一个线程持有锁,一个线程在自旋,又来了第三个线程访问时(反正就是竞争继续加大了),轻量级锁就会膨胀为重量级锁,重量级锁会使除了此时拥有锁的线程以外的线程都阻塞。
升级到重量级锁其实就是互斥锁了,一个线程拿到锁,其余线程都会处于阻塞等待状态。
Monitor(管程)
管程(Monitor)是一种和信号量(Sophomore)等价的同步机制。它在Java并发编程中也非常重要,虽然程序员没有直接接触管程,但它确实是synchronized和wait()/notify()等线程同步和线程间协作工具的基石:当我们在使用这些工具时,其实是它在背后提供了支持。简单来说:
-
管程使用锁(lock)确保了在任何情况下管程中只有一个活跃的线程,即确保线程互斥访问临界区
-
管程使用条件变量(Condition Variable)提供的等待队列(Waiting Set)实现线程间协作,当线程暂时不能获得所需资源时,进入队列等待,当线程可以获得所需资源时,从等待队列中唤醒
所以就解释了为什么synchronized 称为对象锁,原因就是底层Monitor就是寄生在对象上。
https://baijiahao.baidu.com/s?id=1639857097437674576&wfr=spider&for=pc
https://segmentfault.com/a/1190000016417017
API 级别加锁方式二:Lock
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class UseLock implements Runnable {
private static int num = 10;
Lock lock = new ReentrantLock(); // 实例化一个可重锁对象
@Override
public void run() {
for (int i = 0; i < 100; i++) {
lock.lock();
try {
if (num > 0) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "剩余:" + (--num));
}
} catch (Exception ex) {
ex.printStackTrace();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
UseLock useLock = new UseLock();
new Thread(useLock, "线程1").start();
new Thread(useLock, "线程2").start();
new Thread(useLock, "线程3").start();
}
}
Lock 锁:JDK 1.5 后新增,与采用synchronized 相比,Lock 接口类提供了多种锁方案,更加灵活。
注意:
-
如果同步代码发生异常,就要将unlock 方法写入finally 中。
-
Lock 类是一个接口类,需要实例化其实现类才能调用接口类中定义的方法,它拥有与synchronized 相同的并发性与内存语义,但是添加了类似锁投票,定时锁等侯和可中断锁等候等特性,此外它还提供了在激烈争用情况下更佳的性能。
所以优先选用Lock 锁其次是同步代码块最后是同步方法。
ReentrantLock(可重入锁)
为什么使用锁?
如果有2个线程,需要访问同一个对象User。一个读线程,一个写线程。User对象有2个字段,一个是名字,一个是性别。
当User对象刚刚创建出来的时候,姓名和性别都是空。然后,写线程开始填充数据。最后,就出现了以下令人心碎的一幕:
可以看到,虽然写线程先于读线程工作,但是, 由于写姓名和写性别两个操作不是原子的。这就导致读线程只读取了半个数据,在读线程看来,User对象的性别是不存在。
为了避免类似的问题,我们就需要使用锁。让写线程在修改对象前,先加锁,然后完成姓名和电话号码的赋值,再释放锁。而读线程也是一样,先取得锁,再读,然后释放锁。这样就可以避免发生这种情况。
重入锁(到底什么是锁?)
通常情况下,**锁可以用来控制多线程的访问行为。**那对于同一个线程,如果连续两次对同一把锁进行lock,会怎么样了?对于一般的锁来说,这个线程就会被永远卡死在那边,比如:
void handle(){
lock();
lock();
unlock();
unlock();
}
这个特性相当不好用,因为在实际的开发过程中,函数之间的调用关系可能错综复杂,一个不小心就可能在多个不同的函数中,反复调用lock(),这样的话,线程就自己和自己卡死了。
所以,对于希望傻瓜式编程的我们来说,重入锁就是用来解决这个问题的。**重入锁使得同一个线程可以对同一把锁,在不释放的前提下,反复加锁,而不会导致线程卡死。**因此,如果我们使用的是重入锁,那么上述代码就可以正常工作。你唯一需要保证的,就是unlock()的次数和lock()一样多。这样是不是方便很多呢?
其中主要方法:
- lock():加锁,如果锁已经被别人占用了,就无限等待。拿到锁就返回,拿不到就等待。因此,大规模得在复杂场景中使用,是有可能因此死锁的。因此,使用这个方法得非常小心。
如果要预防可能发生的死锁,可以尝试使用下面这个方法:
- tryLock(long timeout, TimeUnit unit):尝试获取锁,等待timeout时间。同时,可以响应中断。
与lock()相比,tryLock()有下面优点:
- 可以不用进行无限等待。直接打破形成死锁的条件。如果一段时间等不到锁,可以直接放弃,同时释放自己已经得到的资源。这样,就可以在很大程度上,避免死锁的产生。因为线程之间出现了一种谦让机制
- unlock():释放锁
重入锁的内部实现,基于CAS
重入锁的核心功能委托给内部类Sync实现,并且根据是否是公平锁有FairSync和NonfairSync两种实现。这是一种典型的策略模式。默认是公平锁有FairSync
实现原理:
基于一个状态变量state。这个变量保存在AbstractQueuedSynchronizer对象中
如果state==0时,表示锁空闲,大于0时表示锁已经被占用,它的数值表示当前线程重复占用这个锁的次数。因此,lock()的最简单的实现是:
compareAndSetState就是对state进行CAS操作,如果修改成功就占用锁,如果修改不成功,说明别的线程已经使用了这个锁,那么就可能需要等待。
acquire() 的实现:
NonfairSync —extends—> Sync —extends—> AbstractQueuedSynchronizer,override:
tryAcquire() 的目的就是再次尝试获取锁,**如果发现锁就是当前线程占用的,则更新state,表示重复占用的次数,同时宣布获得所成功,这正是重入的关键所在!**如果获取锁失败就会在acquireQueued() 中进入队列进行等候,如果在等待过程中被中断了,那么重新把中断标志位设置上。
所以说默认情况下重入锁是不公平的,那到底什么是公平什么是不公平?
-
公平:获取锁是先到先得,按照队列顺序的。
-
不公平:谁得到就是谁的,无顺序、可以插队的。
如果要坚持先到先得的话,那么你就需要在构造重入锁的时候,指定这是一个公平锁:
ReenterantLock rLock = new ReenterantLock(true);
与不公平重入锁的区别在于:一开始不是不管三七二十一,直接抢了再说。而是进入队列;再判断队列中线程前面是否存在线程,如果存在就获锁失败,否则获锁成功。如果发现锁就是当前线程占用的效果同上。但一定要注意,公平锁是有代价的。维持公平竞争是以牺牲系统性能为代价的。
扩展Sync(FairSync公平锁、NonFairSync非公平锁)
锁的接口类
Lock 的实现类
可重锁中的公平锁与非公平锁
-
公平锁:先来后到,按顺序获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
-
非公平锁:可以插队(默认),多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
返回的是new FairSync() 或者 new NonfairSync()
FairSync、NonfairSync均继承Sync
继承族谱:
公平锁
优点:所有的线程都能得到资源,不会消亡在队列中。
缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
源码:
非公平锁
优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
缺点:这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致消亡。
源码:
等待队列实际上是一个双向链表
synchronized与Lock区别
-
Lock 锁是一个类,synchronized 是一个关键字
-
synchronized 无法判断获取锁的状态,而Lock 可以(tryLock)。
-
Lock是显式锁(手动开手动关,不会自动释放),而synchronized 是隐式锁(执行完内部任务才开锁,会自动释放)。
-
Lock 只用代码块锁,而synchronized 不仅有代码块锁还有方法锁
-
使用Lock 锁,JVM 将会花费较少时间来调度线程,所以性能更好,并且具有更好的可扩展性。而synchronized 如果其中有线程阻塞了,其他线程只能等着。