文章目录
- 进程和线程
- 并行和并发
- 同步 异步
- 关于日志文件的配置
- 创建以及运行线程
- 线程运行原理
- 线程的上下文切换
- 线程中的常用方法
- 线程优先级
- 主线程以及守护线程
- 线程的运行状态 5 种? 6 种?
- 阶段小结
- 共享问题
- synchronized
- 线程安全分析
- wait / notify
- 线程状态切换
- 活跃性
- ReentryLock
- 阶段小结:
- JMM Java 内存模型
- 阶段小结
- 乐观锁
- 阶段小结
- 不可变类的使用
- 不可变类的设计
进程和线程
进程
线程
两者之间的对比
线程的上下文切换
当系统的内存不够的时候,可以关闭一些线程,将内存由其他的线程进行使用,这个时候需要进行线程的切换,存在进程上下文的概念;
并行和并发
并行 parallel
多核 cpu 同时执行多个线程,是真正的并发;
并发 concurrent
单核 cpu 进行线程的快速切换
操作系统中存在任务调度器,将 cpu 时间片交给不同的线程执行,cpu 在线程之间切换的速度非常快,人是感觉不到的;
微观串行,宏观并行;
举例说明
并行 并发的测试结果
同步 异步
从方法调用的角度来讲
需要等待结果的返回,才能继续运行的是同步
不需要等到结果的返回,就能继续执行操作是异步
同步在多线程的另外含义:多个线程之间同步进行
关于日志文件的配置
pom
下面的两个配置都是需要的
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration
xmlns="http://ch.qos.logback/xml/ns/logback"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://ch.qos.logback/xml/ns/logback logback.xsd">
<!--控制的输出是 控制台进行输出,实际当中会在日志文件中进行输出-->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!--%date{HH:mm:ss.SSS} %c -->
<!--设置了日志打印输出的时间格式-->
<pattern>%date{HH:mm:ss.SSS} %c [%t] - %m%n</pattern>
</encoder>
</appender>
<!--获取的日志是 debug 级别-->
<!--
name:用来指定受此logger约束的某一个包或者具体的某一个类。
level:用来设置打印级别(TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF),还有一个值INHERITED或者同义词NULL,代表强制执行上级的级别。如果没有设置此属性,那么当前logger将会继承上级的级别。
addtivity:用来描述是否向上级logger传递打印信息。默认是true。
appender-ref则是用来指定具体appender的。
-->
<logger name="c" level="debug" additivity="false">
<appender-ref ref="STDOUT"/>
</logger>
<!--根logger,也是一种logger,且只有一个level属性-->
<root level="ERROR">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
创建以及运行线程
线程的创建以及启动分为两步,先创建,然后运行
thread
@Slf4j(topic = "c.test")
public class Test1 {
public static void main(String[] args) {
Thread t = new Thread() {
@Override
public void run() {
log.debug("running");
}
};
t.setName("t1");
t.start();
log.debug("main is running");
}
}
Runnable
/**
* 线程与任务分离开
*
* Runnable 里面定义了任务
*
* 然后传递到 Thread 进行线程
*/
@Slf4j(topic = "c.Test2")
public class Test2 {
public static void main(String[] args) {
/**
* Runnable r = new Runnable() {
* @Override
* public void run() {
* log.debug("running");
* }
* };
*/
Runnable r = () -> {log.debug("running");};
Thread t = new Thread(r);
t.setName("t2");
t.start();
log.debug("main is running");
}
}
Thread 与 Runnable 之间的关系
Thread 实现的匿名内部类,本质上面还是一种 Thread 类的继承,将线程以及任务放在了一起,使用 Runnable 将线程与任务分开,使得更加的灵活;
@Slf4j(topic = "c.Test12")
public class Test12 {
public static void main(String[] args) throws InterruptedException {
// 实际上使用的是创建的 Thread 线程的构造器 (Runnable,"name")
// 由于里面没有输入的参数,直接使用 lambda 表达式即可
// 任何对象在创建的时候,都是会使用构造函数的,构造函数使用什么的形式,将相关的参数进行传递即可
Thread t1 = new Thread(() -> {
System.out.println("this a new thread");
}
}, "t1");
t1.start();
}
}
FutureTask
接收 Callable 类型的参数,用来处理返回结果的情况
线程之间的通讯可以用的到
@Slf4j(topic = "c.Test3")
public class Test3 {
public static void main(String[] args) throws Exception {
FutureTask<Integer> task = new FutureTask<Integer>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
log.debug("running...");
return 100;
}
});
Thread t = new Thread(task, "t1");
t.start();
// 获取返回结果 主线程得到的返回结果
log.debug("{}", task.get());
log.debug("main is running...");
}
}
线程运行原理
栈与栈帧
线程启动的时候,每个线程都会分配一个虚拟机栈,栈帧就是当前线程调用的每一个方法,在方法调用结束之后,方法会退出来栈,释放内存;
当前线程调用一个方法,这个方法作为一个栈帧放在栈里面;
图解栈与栈帧
public class TestFrams {
public static void main(String[] args) {
method1(10);
}
private static void method1(int x) {
int y = x + 1;
Object m = method2();
System.out.println(m);
}
private static Object method2() {
Object n = new Object();
return n;
}
}
方法调用结束,将 method1 以及 method2 内存释放即可
栈帧小结
线程是有自己的栈帧,自己的程序计数器,相互之间是不会影响的;
线程的上下文切换
什么是线程的上下文切换
线程 A 保存运行状态,切换到线程 B 执行;
从一个线程转换到另外一个线程的执行,叫做线程的上下文切换,保存前一个线程的运行状态,恢复下一个执行的线程的状态;
两个线程运行的切换;
对于线程执行的状态是需要记录的,否则,不知道从什么地方开始执行线程;
线程中的常用方法
join 用于线程之间的通信
start 与 run
start 表示启动线程
run 表示线程启动之后需要执行的代码
启动一个线程必须使用 start() ,直接调用 run 方法,不能启动多线程的执行,只是一个普通的方法执行了;
sleep 与 yield
yield:使得线程从运行状态变成就绪状态,将 cpu 时间片交出来,让其他的线程进行使用;
sleep() 是进入Time Waiting 状态;
Time Wairing 状态是 Java 定义的一种阻塞状态的细分;
yield 的实现依赖于任务调度器;有时候可能出现的结果是,想要将 cpu 时间片让出去,但是,没有其他的线程需要使用,所以出现让不出去的结果;
yield 与 sleep 之间的区别
一个线程调用了 yield 还是有机会继续获得 cpu 时间片继续运行的;
但是 Time Waiting 状态就是 阻塞状态,必须等到时间到了以后,才能执行,在这个期间是没有办法得到 cpu 时间片的;
sleep 是存在等待时间的,但是 yield 是几乎没有等待时间的;
join() 线程的同步可以使用
@Slf4j(topic = "c.Test10")
public class Test10 {
static int r = 0;
public static void main(String[] args) throws InterruptedException {
test1();
}
private static void test1() throws InterruptedException {
log.debug("开始");
Thread t1 = new Thread(() -> {
log.debug("开始");
sleep(1);
log.debug("结束");
r = 10;
},"t1");
t1.start();
// 主线程执行到这里的时候,不会继续执行下去,而是会等待 t1 线程执行结束之后,才会继续执行自己的代码
t1.join();
log.debug("结果为:{}", r);
log.debug("结束");
}
}
主线程就是想要获取到 t1 线程中的数值,怎么比较好的获取到呢?
此时使用 join() 方法即可;
直接使用 sleep() 这个解决方式不是十分的好;
join() 等待线程结束;在上面的代码的体现就是主线程会一直等待知道 t1 线程执行结束;
同步 小案例
上面的案例中,体现出来了同步的概念,在调用了之后,需要等待运行结果,这就是同步;
如何实现多个线程的同步呢?可以使用分别调用多个线程的 join() 方法即可;
多线程运行的时间计算:需要的最长时间的线程的时间就是总运行的时间;
join(long) 有时间限制的等待
当等待的时间超过了限制之后,就不会继续等待了;直接退出去不等待了;
具有时效的等待;
interrupt() 打断 sleep wait join 的线程 阻塞线程
在打断标志的地方,按道理打断标志应该为 true ;但是这种阻塞是的线程在打断之后,报出来了异常,打断为假
interrupt 打断正常运行的线程
结合打断标识,可以主线程可以让分支线程主动的选择是不是确定自己结束后面的代码执行;
@Slf4j(topic = "c.Test12")
public class Test12 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
while (true) {
// 主线程中虽然让 t1 线程进行打断,但是具体是不是打断需要 t1 线程自己决定,主线程打断之后
// 打断标识在这里会变成真的,所以可以使用打断标识进行正确的打断即可
boolean interrupt = Thread.currentThread().isInterrupted();
if (interrupt) {
log.debug("被打断了,退出去循环");
break;
}
}
},"t1");
t1.start();
// 为什么需要主线程先 sleep() 一秒钟呢?为了让 t1 线程先运行一下,不然 t1 还没有sleep 就打断了 程序没有达到自己的期望输出
Thread.sleep(1);
log.debug("interrupt");
// 这个时候使用了打断标识,可以合理的将线程进行打断处理
t1.interrupt();
}
}
interrupt 的多线程设计模式 两阶段终止模式 (Two Phase Termination)
简单理解就是一种好的使用一个线程停止另外一个线程的方法;
这个好的方法就是使用 两阶段终止莫斯
监控线程的案例
上面的错误思路是,stop 以及 exit 导致程序错误的执行,是不建议使用的;
每隔一段时间进行线程的监测,观察是否打断线程;
后面的存在异常之后,打断标记会变为 false 此时可以重新对于 false 进行处理将其改变为 true 或者不进行改变;当程序运行到打断标记为 true 的时候,线程才会被终止;
@Slf4j(topic = "c.Test13")
public class Test13 {
public static void main(String[] args) throws InterruptedException {
TwoPhaseTermination twoPhaseTermination = new TwoPhaseTermination();
twoPhaseTermination.start();
Thread.sleep(3500);
twoPhaseTermination.stop();
}
}
@Slf4j(topic = "c.Two")
class TwoPhaseTermination{
private Thread monitor;
// 启动监控线程
public void start() {
monitor = new Thread(()->{
while (true) {
Thread current = Thread.currentThread();
if (current.isInterrupted()) {
log.debug("终止当前线程的执行");
break;
}
try {
Thread.sleep(1000);
log.debug("执行监控记录");
} catch (InterruptedException e) {
e.printStackTrace();
// 重新设置打断标志,因为是在异常中执行的,打断标志为 false,重新设置之后,为了打断标志为 真
// 打断标志会变为 true 线程才会被真正的终止
current.interrupt(); // 两次打断 终止线程的执行
}
}
});
monitor.start();
}
// 关闭监控线程
public void stop() {
// monitor 就是监测线程,这个是正常调用线程的 interrupt 方法,其他的方法也是可以调用的;
monitor.interrupt();
}
}
运行结果:
19:58:05.922 c.Two [Thread-0] - 执行监控记录
19:58:06.930 c.Two [Thread-0] - 执行监控记录
19:58:07.932 c.Two [Thread-0] - 执行监控记录
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at com.luobin.concurrent.TwoPhaseTermination.lambda$start$0(Test13.java:38)
at java.lang.Thread.run(Thread.java:748)
19:58:08.421 c.Two [Thread-0] - 终止当前线程的执行
interrupt 打断 park 线程
@Slf4j(topic = "c.Test15")
public class Test15 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
log.debug("parking");
LockSupport.park();
log.debug("unpark");
log.debug("打断状态:{}",Thread.interrupted());
// 上面的 interrupted 修改为 false 下面的 park 会继续的执行
// park 会继续执行
LockSupport.park();
log.debug("unpark");
},"t1");
t1.start();
// 放置还没有进入 park 直接打断
Thread.sleep(1000);
// 打断 park 的状态
t1.interrupt();
}
}
小结:
park 方法本身会使得当前线程进入阻塞状态;
Thread.currentThread().isInterrupted()
会打断 park 线程;使得线程打断,停止运行;
Thread.interrupted() 不会打断 park park 会继续的执行;
不推荐使用的方法 过时的方法
容易导致线程死锁的产生,不建议使用;过时的方法
线程优先级
可以设置优先级别,最低的优先级别是 1 ,最高的优先级别是 10,默认的优先级别是 5 ;
线程的优先级,会提示调度器调度该线程,仅仅是一个提示,调度器是可以忽略你的提示的;
在 cpu 比较忙的时候,线程的优先级高的线程会获得更多的时间片,但是在 cpu 比较闲的时候,优先级几乎是没有作用的;
@Slf4j(topic = "c.Test9")
public class Test9 {
public static void main(String[] args) {
Runnable task1 = () -> {
int count = 0;
for (;;) {
System.out.println("---->1 " + count++);
}
};
Runnable task2 = () -> {
int count = 0;
for (;;) {
// Thread.yield();
System.out.println(" ---->2 " + count++);
}
};
Thread t1 = new Thread(task1, "t1");
Thread t2 = new Thread(task2, "t2");
t1.setPriority(Thread.MIN_PRIORITY);
t2.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
}
}
小结
无论是 yield 还是设置的优先等级,都不能最终的决定任务调度的最终结果,只有任务调度器本省可以决定;
yield 以及 线程优先级仅仅是对于线程调度器的一个提示而已;
案例 防止 CPU 占用率达到 100%
只是编写 while true 存在问题中间可以加一个 sleep() 避免空转;
主线程以及守护线程
在默认的情况下面,Java 进行需要等待所有的线程执行完毕之后,才会结束进行的运行,但是不会等待守护线程的结束,守护线程没有执行结束,也是会被强制结束的,备胎舔狗线程;
线程的运行状态 5 种? 6 种?
操作系统的层面
从 Java API 层面进行描述
Java 源代码中将线程的运行状态分为 6 种,使用枚举记性分类的;
NEW 表示线程刚创建,没有使用 start 方法
RUNNABLE 当调用了 start() 方法之后,注意,Java API 层面的 RUNNABLE 状态涵盖了 操作系统 层面的【可运行状态】、【运行状态】和【阻塞状态】(由于 BIO 导致的线程阻塞,在 Java 里无法区分,仍然认为是可运行)
BLOCKED , WAITING , TIMED_WAITING 都是 Java API 层面对【阻塞状态】的细分
TERMINATED 当线程代码运行结束
6 种状态的代码演示
@Slf4j(topic = "c.TestState")
public class TestState {
public static void main(String[] args) throws IOException {
// new
Thread t1 = new Thread("t1") {
@Override
public void run() {
log.debug("running...");
}
};
// runnable
Thread t2 = new Thread("t2") {
@Override
public void run() {
while(true) { // runnable
}
}
};
t2.start();
// Terminate
Thread t3 = new Thread("t3") {
@Override
public void run() {
log.debug("running...");
}
};
t3.start();
// 有时间限制的等待
Thread t4 = new Thread("t4") {
@Override
public void run() {
synchronized (TestState.class) {
try {
Thread.sleep(1000000); // timed_waiting
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t4.start();
// 需要等待 线程 t2 的执行结束才会停止执行代码。所以处于 waiting z状态
Thread t5 = new Thread("t5") {
@Override
public void run() {
try {
t2.join(); // waiting
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
t5.start();
// TestState.class 先被 t4 线程加锁,然后下面的这个线程拿不到锁,所以到了 blocked(被封锁) 的状态
Thread t6 = new Thread("t6") {
@Override
public void run() {
synchronized (TestState.class) { // blocked
try {
Thread.sleep(1000000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t6.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("t1 state {}", t1.getState());
log.debug("t2 state {}", t2.getState());
log.debug("t3 state {}", t3.getState());
log.debug("t4 state {}", t4.getState());
log.debug("t5 state {}", t5.getState());
log.debug("t6 state {}", t6.getState());
System.in.read();
}
}
执行结果
11:59:52.996 c.TestState [t3] - running...
11:59:53.501 c.TestState [main] - t1 state NEW
11:59:53.502 c.TestState [main] - t2 state RUNNABLE
11:59:53.502 c.TestState [main] - t3 state TERMINATED
11:59:53.502 c.TestState [main] - t4 state TIMED_WAITING
11:59:53.502 c.TestState [main] - t5 state WAITING
11:59:53.502 c.TestState [main] - t6 state BLOCKED
阶段小结
共享问题
共享资源带来的问题
代码模拟实现
@Slf4j(topic = "c.Test17")
public class Test17 {
static int count = 0;
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(()-> {
for (int i = 0; i < 5000; i++) {
count++;
}
},"t1");
Thread t2 = new Thread(()-> {
for (int i = 0; i < 5000; i++) {
count--;
}
},"t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("count 现在的数值是:{}",count);
}
}
上面的代码中,结果不一定是 0 ,下面是分析,涉及到线程安全的问题:
虽然做了一次加法以及一次减法,但是由于指令的交错,使得最终的结果显示的不正确;
一个加法线程在执行结束之后,还没有来得及写入更新,线程进行了上下文切换,在另一个减法线程写入更新前,加法的线程更新了,随后减法的线程更新了,导致最终是减法的结果,加法的线程被覆盖掉了,产生了不正常的状态;
下面展示为 结果为 1 的情况,这种情况也是不正确的:
虽然做了一次加法以及一次减法,但是由于指令的交错,使得最终的结果显示的不正确;
根本原因就是上下文切换导致的指令交错,导致了错误的执行结果;造成了多线程访问安全的问题;
临界区 Critical Section
多个线程共享的时候,发生了指令交错,引发出来的临界区的概念;
一段代码存在对共享资源的 多线程 读写操作,叫做临界区
竞态条件
多个线程在临界区内执行,由于代码的执行序列不同,从而产生的导致结果无法预测,叫做竞态条件;
避免临界区的竞态条件的发生,采用下面的处理方式
1、阻塞方式的解决方案
2、非阻塞方式的解决方案
synchronized
保证在里面的对象是将来多线程访问的同一个变量;
同一个时刻,只能有一个线程拿到对象锁,其他线程是访问不到的;其他的线程进入阻塞状态,也就是所谓的 block 状态;
这样子避免了还没有来得及保存数据就发生了上下文切换,导致的数据返回结果的问题;
当前线程使用结束之后,释放锁,让其他的线程得到对象锁,继续进行访问;
使用图解进行上述 synchronized 的解释
上述图的含义就是:一个线程必须将所有的代码块中的代码执行结束之后,才会把锁交还回去,其他线程处于阻塞状态是获取不到锁的,因为cpu 时间片不会分配给阻塞线程,只有当前线程执行完毕,他可以唤醒阻塞线程,进行下面的相关操作;
使用严谨的方式进行表示 (参考黑马程序员)
思考
synchronized 实际上是使用了对象锁,保证了临界区域内代码的原子性,临界区域的代码对于外面是不可以分割的,不会被线程切换打断;
下面的问题是对于加不加对象锁的讨论,只有多个线程同时加了对象锁,那么才能保证临界区代码执行的原子性,才不会产生由于线程上下文切换锁导致的问题;
使用面向对象的思想进行上面的代码优化
@Slf4j(topic = "c.Test17")
public class Test17 {
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.increment();
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.decrease();
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("count 现在的数值是:{}",room.getCounter());
}
}
class Room {
private int count = 0;
public void increment() {
// 锁住当前的对象 this 指代的就是当前的 Room 对象
synchronized (this) {
count++;
}
}
public void decrease() {
synchronized (this) {
count--;
}
}
public int getCounter() {
synchronized (this) {
return count;
}
}
}
synchronized 加在方法上
synchronized 是对象锁,只是锁住对象,不是锁方法的,可以在方法书写的时候,使用 synchronized 进行申明这是锁住的对象,也就是锁住了方法中的所有代码块;
锁住的是 this 对象;
将 synchronized 加在静态方法上面,也是同样的道理;只不过这个时候锁住的是类对象,不是当前对象,类对象是:Test.class
类对象以及普通对象的理解
类:定义了一套规则
类对象:.class 文件
普通对象:.class 文件实例化出来的普通对象
理解的不太准确,后续仍然需要理解;
线程八锁 synchronized 的理解
锁住了 this
5、一个使用的是类锁,一个使用的是对象锁,两者之间是没有影响的,所以先执行 2 等待 1 s 之后,执行 1
6、使用的都是类锁,也就是使用的是一把锁,所以两者之间会灯虎等待
小结上面的 synchronized 的问题,就是先要清楚,获得的是什么锁,多个线程使用一把锁需要相互等待,使用不同的锁是不需要等待的;
一定需要分析是不是一把锁,是类锁还是对象锁,这个是十分重要的,并且锁的对象是不是同一个;
线程安全分析
变量的线程安全分析
局部变量的线程安全分析
下面的情况中,i 在栈中没有被共享,所示是不存在线程安全的问题的;
不存在线程安全的问题;
list 是成员变量
list 的创建在类的内部,引用关系,可能是下图所示:
如果局部变量引用的是一个对象,那么可能产生线程安全的问题,因为对象是保存在堆内存空间的,可能导致堆内存共享,进而导致的线程安全问题;
list 是局部变量,局部创建出来的
list 也就是在 方法体重创建出来的;
局部变量是引用的情况之下,如果这个引用是局部变量,也就是创建出来的对象是在局部中创建的,那么是没有线程安全问题的,
但是如果将局部变量的引用暴露出去,是有可能发生线程安全的问题的;
这个暴露出去指的是由于创建了子类,子类中开启了一个新线程,不能控制子类的行为,局部变量的引用可能同时被多个线程访问到,进而存在线程安全的问题;
privite 的方法在子类中是没有办法重写的,可以保护线程的安全;
防止子类进行方法的重写,可以在方法上面加上去 final;
加上去 final privite 可以起到一定的线程安全的作用;
常见的线程安全类的学习
需要注意两点:
1、方法是原子的
2、多个方法组合到一起不是原子的,因为多个方法的组合没有 sychronized 进行修饰;
下面的方法的组合使用,可能由于线程的上下文切换导致的线程安全问题的产生;
不可变类线程安全性
String 怎么保证线程安全的?
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
没有改变原来的 value 的数值,而是直接复制创建出来的新的字符串,所以说 String 是不可变的;
不可变类都是线程安全的,因为其值是不能被改变的,所以是安全的;
线程安全判断
HashMap 不安全
Date 加不加 final 都是不安全的,因为日期会发生变化,加上去 final 只是说明了引用是不会发生变化的,里面的内容是有可能发生变化的;
属性可以修改的类产生的对象都是线程不安全的;
UserServiceImpl 都是线程不安全的;
因为里面的 count 属性是可以进行修改的,所以是不安全的;
存在线程安全,单例模式下面,会被多个线程访问,所以可能产生问题,牵扯到对象的成员变量的修改,就会产生线程安全的问题;
没有成员变量的类,一般都是线程安全的;观察里面的局部变量,方法内部的局部变量,
最好是将 Connection 变成私有的局部变量,而不是共享的成员变量;
使用 final privite 一定程度上面可以是线程安全的;
使用线程安全写一个简单的卖票程序
@Slf4j(topic = "c.ExerciseSell")
public class ExerciseSell {
public static void main(String[] args) throws InterruptedException {
// 模拟多个人同时进行买票
TicketWindow1 ticketWindow = new TicketWindow1(1000);
// 将所有的线程放到集合中
// 因为 这个 ArrayList 不会被多个线程进行访问,所以这里可以使用这个;
// 多线程下面就不能使用这个了
List<Thread> threadList = new ArrayList<>();
// 将卖出去的票放入到 List 里面,使用线程安全的 Vector
List<Integer> sellAmount = new Vector<>();
for (int i = 0; i < 200; i++) {
Thread thread = new Thread(()->{
// ticketWindow ticketWindow 是两个共享变量,所以不存在方法的组合之后就变得线程不安全
// 如果这两个变量是同一个变量的两个方法那么就会产生错误
int count = ticketWindow.sell(randomAmount(5));
sellAmount.add(count);
});
Thread.sleep(randomAmount(5));
threadList.add(thread);
thread.start();
}
// 将所有的线程添加到主线程中
// 将线程全部加入到主线程是为了得到所有线程的运行结果,使得线程安全,否则全部线程的运行结果可能出错
for (Thread thread : threadList) {
log.debug("当前线程是:" + thread.getName());
thread.join();
}
// 统计卖出去的票以及剩下来的票加起来等于 100000 说明是安全的;
log.debug("剩余的票是: " + ticketWindow.getCount());
log.debug("卖出去的票是 :" + sellAmount.stream().mapToInt(i -> i).sum());
}
static Random random = new Random();
// Random 返回伪随机数
public static int randomAmount(int amount) {
return random.nextInt(amount) + 1;
}
}
class TicketWindow{
private int count;
public TicketWindow(int count) {
this.count = count;
}
public int getCount() {
return count;
}
// 关于卖票 这个一定是需要是线程安全的;
public synchronized int sell(int amount) {
if (this.count >= amount) {
this.count -= count;
return amount;
} else {
return 0;
}
}
}
***深入学习 synchronized 底层 - Monitor 概念
32 位虚拟机中
对于所谓的数组对象在 Klass 后面还会记录一个数组的长度;
其中的 MarkWord 的具体形式如下所示: 这个结构是比较重要的,因为一个对象充当锁对象的时候,随着锁的升级,这个对象头中的 MarkWord 的内容是会发生变化的;
markWord 32 位,有下面的 5 种情况,每种情况的 32位不变,但是内容的分布有所改变;
发生改变的原因就是,线程访问锁对象的时候,随着从 偏向锁 -> 轻量级锁 -> 重量级锁的升级,MarkWord 对于这个锁的标记也会发生变化,锁都变了,这个锁对象的 MarkWod 改变是应该的;
上图是轻量级锁进行 cas 交换之后的结果;
轻量级锁的情况就是,线程的锁记录对象里面的 锁记录地址 和 MarkWord 进行替换;
线程的锁记录对象的 Object Reference 指向锁对象;
注意区分一个指向锁对象的,一个是j交换锁对象头的MarkWord;
每次都进行 cas 的交换,对于系统的开销比较大, 所以优化成为偏向锁
这个时候,交换的是 线程 id 减少了很多的 cas 操作;
上面的轻量级锁以及 偏向锁的时候,没有与 Monitor 对象进行关联,继续升级,成为重量级锁,与Monitor 对象进行关联,有了后面的 WaitSet 以及 EntrySet 队列;
偏向锁适合一个一个线程总是对一个对象锁进行访问;
轻量级锁,适合多个线程,但是线程之间的访问是交错运行的;
重量级锁是适合多个线程,同时访问对象锁;
JDK 1.6 对这个进行优化,直接一上来就使用重量级锁,对于整个资源的消耗是很大的;
Monitor 在底层就是一个锁 翻译叫做监视器或者管理;
每个 Java 对象都是可以关联一个 Monitor 对象的;
- 当一个线程访问临界区代码块的时候,它是第一个访问这个临界区代码块的,他这个时候会获得 obj 对象对应的监视器 Monitor ,获得之后就会一直占有 这个 obj 的锁,这个进程与 Monitor 的 Owner 进行关联;后面进来的 Thread-1 Thread-3 ,由于没有锁,所以只能进入等待的状态,也就是阻塞状态 BLOCKED;
Thread-2 执行结束之后,就会将 Owner 空出来,让等待队列的 Thread-1 Thread-3 其中的一个叫醒,然后竞争 cpu 时间片,是不是能占用 Monitor 里面的锁;
竞争之后,假设 Thread-1 获得了锁,然后 Thread-3 继续等待,具体什么线程会获得锁,根据 JVM 的具体实现即可判断;
竞争一般是非公平的;
Monitor 总是会充当一个锁的角色,因为对象会与锁进行关联;
同一个对象会和同一个 Monitor 之间进行关联
上面执行过程的小结
注意:
1、 synchornized 必须进入同一个对象的 Monitor 才会有上面的效果,进而实现线程安全
2、不加 synchornized 的对象不会关联监视器,是不会遵守上面的规则的;
***从Java 字节码的角度学习 Monitor
原始的 Java 代码
反编译之后的字节码指令
public class com.luobin.concurrent.Test20 {
static final java.lang.Object lock;
static int counter;
public com.luobin.concurrent.Test20();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #7 // Field lock:Ljava/lang/Object;
3: dup
4: astore_1 // 记录 Owner指向的指针
5: monitorenter
6: getstatic #13 // Field counter:I
9: iconst_1
10: iadd
11: putstatic #13 // Field counter:I
14: aload_1
15: monitorexit
16: goto 24
19: astore_2
20: aload_1
21: monitorexit
22: aload_2
23: athrow
24: return
Exception table: // 发生异常的话执行 19 -22 正常执行 6 - 16
from to target type
6 16 19 any
19 22 19 any
static {};
Code:
0: new #2 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: putstatic #7 // Field lock:Ljava/lang/Object;
10: iconst_0
11: putstatic #13 // Field counter:I
14: return
}
synchronized 使用的重量级锁的优化
由于重量级锁的加上去,导致系统的整体的运行效率是比较低下的,这个时候进行优化,提出来了轻量级锁,偏向锁等概念;
轻量级锁优化实现对重量级锁的优化
使用轻量锁的核心是,虽然是多线程可以进行访问,但是访问的时间是错开的,可以使用轻量锁;
轻量级锁失败之后,会加上去一个重量级锁;
轻量级锁的实现原理
Object(这个对象用来充当锁,也就是 synchornized 里面锁住的对象,在底层这个对象会和Monitor进行关联)
里面存在对象头,对象体;
Lock Record 里面保存锁对象 Object 的引用地址以及 要加锁的对象的 MarkWord(在对象头里面)
(Mark Word 的介绍上面存在的)
创建的锁记录对象
不是 Java 层面的,是 JVM 层面创建的;
尝试将锁记录进行替换,从(01)无锁状态替换为轻量级锁;
原来的创建出来的对象(Object)的锁从 01(无锁) 变为了 00 变成了轻量级锁;
转换成功表示的是:从无锁的状态 01 转换到了轻量级锁状态 00
可以观察到的是:Lock Record 对象里面的第一行的数据和 Object 对象头中的数据进行了交换;达到了加上去轻量级锁;
cas 操作,保证交换的过程是原子性的,保证同时成功或者同时失败;
如果是自己再次进行了锁重入,加一个 Lock Record 作为重入的计数;
这个时候的 cas 交换是不会成功的,因为他自己拿着锁,在上一个 Lock Record 对象中;
锁记录可以看出来,对于同一个对象加了多少把锁;
锁膨胀
在尝试加上去轻量级锁的过程中, CAS 操作无法完成,这个时候可能就是因为其他的线程为这个对象加上去了对象锁,存在了竞争,这个时候便产生了锁膨胀,此时需要将轻量级锁转换为重量级锁;
加锁的目的,锁的转换的目的就是一方面保持着一定的性能,另外一方面考虑到线程安全的问题;
这个时候,这个 object 对象就会加一个重量级别的锁,此时 Monitror 才会和这个 Object 进行关联,新进来的线程就可以到 EntryList 进入等待的状态,找了一个休息的地方;
如果没有将轻量级锁转换为重量级锁,那么新进来的线程就没有地方去了,但是这个线程还是需要运行的,所以不得不转换成重量级锁;
Monitor 地址后两位变成了(10) 也就是变成了重量级锁;
自旋操作实现优化重量级锁
当重量级锁在竞争的时候,可以使用自旋来进行优化,如果当前线程自旋成功(也就是持有锁的线程已经退出了同步块,释放了锁)
,这个时候就可以避免阻塞,避免了阻塞之后,在多核 CPU 下面可以提升性能;
没有优化之前,一个线程成为了 Monitor 的Owner ,其他的线程想要访问的时候,需要进入到 EntryList 进行阻塞等待,直到当前线程释放了锁,他才会被唤醒,可能执行自己的相关内容;
所谓的自旋
,就是先不要直接进入阻塞的链表中,先尝试看当前线程有没有释放锁,释放之后,直接执行自己的代码块,不要进入阻塞链表,减少的线程的唤醒次数,可以节约性能;
自旋的操作,适合多核 CPU ,是不适合单核 CPU 的;单核下面是没有意义的;
当进行了几次的自旋之后,还是不能执行自己的线程,那么就进入阻塞状态;
在 Java 的底层中,自旋是没有办法控制的,底层的自旋是比较智能的,自适应的;比如对象刚刚的一次自旋成功了,后面这个线程访问的时候,自旋成功的可能性就会大一些;反之,可能少自旋或者不自旋;
自旋本身是会占用 CPU 时间的,单核下面是浪费时间,多核 CPU 才能发挥优势;
Java 7 以后不能控制是不是开启自旋功能;
偏向锁实现优化轻量级锁
轻量级别锁在没有竞争的时候(只有当前线程一个线程),每次的重入仍然是需要 cas 操作的,每次都执行 cas 操作是比较消耗资源的,这个时候考虑到使用偏向锁进行优化了;
偏向锁:只有第一次使用 cas 线程将线程 ID 设置到 Mark Word 的头部,之后发现这个线程的 ID 是自己
,还是自己的重入,那么就不用实现 cas 操作了,之后不发生竞争,这个对象就归线程所有,减少了 cas 执行的次数,提高了运行效率;
这个时候在锁对象的头部分设置的是 线程 ID ,不是下面这 5 行里面的任意一行数据了,以前的轻量级锁是下面 5 行中的 第三行:Lightweught Locked
;
换成ID 就是为了识别达到减少 cas 操作;
以后这个对象偏向于当前线程,起名曰偏向锁;
这个 cas 操作指的是 Lock Record 里面的地址和 Object Head里面 的Mard Word 内容进行交换,偏向锁可以减少这个交换;
下面表示的轻量级锁的每次对于这个 Object 的访问,每次都是需要 cas 操作的
下面的这个是使用了偏向锁的对于 Object 的访问,第一次的使用,将线程ID 放到对象的头部,以后访问直接看是不是同一个线程访问,是同一个线程访问的话,就不需要 CAS 操作了;
偏向锁的使用场景:只有一个线程使用的时候或者在冲突比较少的时候,使用偏向锁比较好,因为可以减少 CAS 的操作;
但是在多线程下面,不并且经常的伴随着锁的相互竞争,使用偏向锁是不太好的;
Java 程序启动的时候,Java 中可以禁止使用偏向锁的使用,在多线程的时候,一上来就是正常对象,可以设置禁用;默认是启用偏向锁的;
使用锁的顺序是:偏向锁 -> 轻量级锁 -> 重量级锁(自旋锁可以优化)
偏向锁的撤销
1 为什么调用了充当锁的对象的 hashCode 之后会禁用偏向锁? 101 -> 001
可偏向的对象调用了 hashCode() 之后,由于字节占满了,导致偏向锁失效;
原来的 64 位在下面的对象头中,hashCode 占用 31 ,但是如果把 hashCode 加入到对象头中,超过了 64 位,所以取消了偏向锁,保持 hashCode 可以保存进去;没有地方存储 hashCode 只能放弃偏向锁;
轻量级锁以及重量级锁都有额外的空间,可以存储 hashCode ,但是偏向锁是没有的,所以放弃了偏向锁;
2 其他线程使用偏向锁时候,偏向锁升级为轻量级锁
设置案例的时候,两个线程访问一个锁对象,需要错开调用,否则直接将锁转换为重量级锁,是不对的;
错开之后,偏向锁会升级为轻量级锁;
01 轻量级锁;
00 偏向锁;
10 重量级锁;
偏向锁的重偏向
对象被多个线程访问,但是访问之间是没有竞争的,会发生取消对当前线程偏向,重新偏向当前线程的操作,这个操作多了之后,对于系统性能的损耗还是比较大的,这个时候会考虑到是不是偏向错了;
这个时候会偏向到新的线程,减少系统的消耗,偏向给原来的线程,但是它来时反复跳,超过了容忍(阈值),换一个线程进行偏向,对于后面的线程进行批量的偏向,减少加锁,解锁之间的消耗;
偏向锁的批量撤销偏向
达到撤销的阈值之后,偏向锁会进行批量的撤销;
锁消除
JIT 会对热点代码进行优化处理,默认是打开的状态,这个时候在循环一定的次数的情况下,加不加 synchronized 结果是差不多的;
当把这个热点代码优化的开关关闭之后,不进行优化,加不加 synchronized 的效果是比较明显的;
所谓的锁消除
的优化就是:
将 synchronized 里面的代码块进行优化处理,提高性能,当这种锁消除
优化关闭之后,性能的差距是十分明显的;所以还是使用默认的锁消除优化比较好,在一定程度上;
wait / notify
WAITING 已经获得了锁,但是没有满足条件(所谓的不满足条件是程执行过程中,不满足某些程序的执行流程,比如等待其他线程的执行结果,这个时候,其他线程还没有执行结束,当前线程得不到需要的数据,所以会发生阻塞)
,放弃了锁,进入了 WaitSet 的队列中;
BLOCKED 没有获得锁在阻塞的队列中;
WAITING BLOCKED 都是处于阻塞状态,是不会占用 cpu 时间片的;
唤醒之后,进入 BLOCKED 的队列中,不是直接运行的;
几个 API 介绍
obj.wait () 调用这个线程的方法进入 WaitSet (休息房间进行等待)
obj.notify() 挑一个唤醒;
obj.notifyAll() 在 WaitSet 里面等待的所有线程都唤醒;
上面的三个方法都是Object 对象的方法,是为了保证线程之间协作的手段,必须获得了这个对象的锁之后,才能调用Obj.wait() 这几个方法;
调用 wait() 方法的时候,是需要获得 Owner 的锁的;
调用 notify() 的时候,也是其他的线程获得了 Owner 锁,才有资格调用这个方法,唤醒在 WaitSet 队列中的线程;
sleep(long n) wait(long n) 之间的区别
1、属于的类不一样
2、强不强制使用 synchronized
3、释放不释放锁
下面的代码说明了 sleep() 方法是不会释放锁的;
但是 wait() 方法是会释放锁的;
4、sleep() 以及 wait 状态是一样的 TIMED_WAITING 有时限的等待;
@Slf4j(topic = "c.Test18")
public class Test18 {
// 希望锁住的对象一直是同一个对象,所以加上去了 final 不可以发生变化
static final Object lock = new Object();
public static void main(String[] args) throws Exception {
new Thread(() -> {
try {
log.debug("新建的线程获取到了锁");
synchronized (lock) {
// sleep() 方式是不会释放锁的,其他的线程来了需要去 BLOCKED 等待
Thread.sleep(20000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
Thread.sleep(1000);
synchronized (lock) {
log.debug("主线程获取到了锁");
}
}
}
20:45:41.748 c.Test18 [Thread-0] - 新建的线程获取到了锁
@Slf4j(topic = "c.Test18")
public class Test18 {
// 希望锁住的对象一直是同一个对象,所以加上去了 final 不可以发生变化
static final Object lock = new Object();
public static void main(String[] args) throws Exception {
new Thread(() -> {
try {
log.debug("新建的线程获取到了锁");
synchronized (lock) {
// sleep() 方式是不会释放锁的,其他的线程来了需要去 BLOCKED 等待
lock.wait(20000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
Thread.sleep(1000);
synchronized (lock) {
log.debug("主线程获取到了锁");
}
}
}
20:44:51.305 c.Test18 [Thread-0] - 新建的线程获取到了锁
20:44:52.307 c.Test18 [main] - 主线程获取到了锁
同步模式 - 保护性暂停
用作一个线程等待另外一个线程的执行结果中;
保护型暂停模式相比较 join 的方式,有更好的优点
下面的代码的含义是:线程 t1 等待线程 t2 执行结束之后,t1 才继续执行;
@Slf4j(topic = "c.Test20")
public class Test20 {
public static void main(String[] args) {
// 创建出来的同一个 guardedObject 对象,需要保证线程安全;
GuardedObject guardedObject = new GuardedObject();
new Thread(() -> {
// 获取 response 的时候,需要等待另外一个线程返回结果;
log.debug("等待下载结果");
List<String> list = (List<String>) guardedObject.get();
log.debug(String.valueOf(list.size()));
}, "t1").start();
new Thread(() -> {
log.debug("执行下载");
try {
List list = download();
// 将数据传递过去之后,就会将 wait 状态的线程唤醒;
guardedObject.complete(list);
} catch (IOException e) {
e.printStackTrace();
}
}, "t2").start();
}
public static List<String> download() throws IOException {
HttpURLConnection conn = (HttpURLConnection) new URL("https://www.baidu.com/").openConnection();
List<String> lines = new ArrayList<>();
try (BufferedReader reader =
new BufferedReader(new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
lines.add(line);
}
}
return lines;
}
}
class GuardedObject {
// 结果
private Object response;
public Object get() {
synchronized (this) {
while (response == null) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 不用等待了,因为有数值可以获取了
// 因为 response 有了值,不用等待,可以拿到这个 response;
return response;
}
public void complete(Object response) {
synchronized (this) {
// 给结果变量赋值
this.response = response;
// 因为赋值好了,所以可以叫醒需要获取的线程
this.notifyAll();
}
}
}
防止t1 线程一直进入到等待线程中,使用超时处理,超过一定的时间,退出去;
class GuardedObject {
// 结果
private Object response;
public Object get(long timedOut) {
synchronized (this) {
while (response == null) {
// 记录开始时间
long begin = System.currentTimeMillis();
// 经历的时间
long passTime = 0; // 刚开始还没有等待,所以是 0 ;
// 经过的时间大于设定的超时时间,超时了,直接打断即可
long waitTime = timedOut - passTime;
// timeOut = passTime + waitTime;
if (waitTime <= 0) {
break;
}
try {
// 等待是为了获取线程 2 的执行结果
this.wait(waitTime); // 因为存在虚假唤醒, while 可以控制虚假唤醒的次数,如果直接 是 timeOut 每次来都等待这个长时间
// 早就超过了 timeOut 给定的时间,防止虚假唤醒的操作// 比如唤醒之前等待了 1 s 这里应该等待 timeOut - passTime 不是等待 timeOut 秒;
} catch (InterruptedException e) {
e.printStackTrace();
}
// 经历的时间
passTime = System.currentTimeMillis() - begin;
}
}
// 不用等待了,因为有数值可以获取了
// 因为 response 有了值,不用等待,可以拿到这个 response;
return response;
}
public void complete(Object response) {
synchronized (this) {
// 给结果变量赋值
this.response = response;
// 因为赋值好了,所以可以叫醒需要获取的线程
this.notifyAll();
}
}
join 方法的原理 保护性暂停模式
源代码 和上面的保护性暂停有异曲同工之妙;
只不过保护性暂停等待的是另一个线程的执行结果;
join() 方法等待的是另一个线程的执行结束;
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
保护性暂停模式的扩展
生产者 消费者模式 异步模式
线程之间是需要相互进行通信的,产生一个消息,其他的线程马上开始工作,这个是保护性暂停的方式,缺点是产生一个,消费一个,使得效率不是十分的高,为了达到高效率,产生了下面的消息队列的模式;产生的消息放在队列中即可,没有必要产生一个消费一个;
与进程之间的消息队列不同,这个是属于线程之间的消息队列,是一种比较简单的实现方式;
前面的产生以及消费是一一对应的;
使用生产者,消费者模式;
JDK 里面的各种阻塞,采用的是这种模式;
测试类
@Slf4j(topic = "c.Test21")
public class Test21 {
public static void main(String[] args) {
MessageQueue queue = new MessageQueue(100);
for (int i = 0; i < 3; i++) {
int id = i;
new Thread(() -> {
queue.put(new Message(id , "值"+id));
}, "生产者" + i).start();
}
new Thread(() -> {
while(true) {
sleep(1);
Message message = queue.take();
}
}, "消费者").start();
}
}
消息队列的创建,用来存放以及消费数据
// 消息队列类 , java 线程之间通信
@Slf4j(topic = "c.MessageQueue")
class MessageQueue {
// 消息的队列集合
private LinkedList<Message> list = new LinkedList<>();
// 队列容量
private int capcity;
public MessageQueue(int capcity) {
this.capcity = capcity;
}
// 获取消息
public Message take() {
// 检查队列是否为空
synchronized (list) {
while(list.isEmpty()) {
try {
log.debug("队列为空, 消费者线程等待");
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 从队列头部获取消息并返回
Message message = list.removeFirst();
log.debug("已消费消息 {}", message);
list.notifyAll();
return message;
}
}
// 存入消息
public void put(Message message) {
synchronized (list) {
// 检查对象是否已满
while(list.size() == capcity) {
try {
log.debug("队列已满, 生产者线程等待");
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 将消息加入队列尾部
list.addLast(message);
log.debug("已生产消息 {}", message);
list.notifyAll();
}
}
}
定义的存放的消息对象的内部结构
final class Message {
private int id;
private Object value;
public Message(int id, Object value) {
this.id = id;
this.value = value;
}
public int getId() {
return id;
}
public Object getValue() {
return value;
}
@Override
public String toString() {
return "Message{" +
"id=" + id +
", value=" + value +
'}';
}
}
park Unpark
Park() unpark()
是LockSupport 类里面的方法
@Slf4j(topic = "c.TestPark")
public class TestPark {
public static void main(String[] args) {
Thread t1 = new Thread(()->{
log.debug("start");
Sleeper.sleep(1);
log.debug("park...");
LockSupport.park();
log.debug("resume");
},"t1");
t1.start();
Sleeper.sleep(1);
log.debug("unPark...");
LockSupport.unpark(t1);
}
}
执行结果:
16:28:17.977 c.TestPark [t1] - start
16:28:18.979 c.TestPark [t1] - park...
16:28:18.979 c.TestPark [main] - unPark...
16:28:18.980 c.TestPark [t1] - resume
执行了 park 之后,线程进入休眠状态,当其他的线程调用了 unpark() 方法之后,此时会将休眠的线程重新开始执行;
主线程在一秒之后 unPark ;t1 线程在两秒之后 park ;结果是:线程 t1 继续执行,没有被打断执行;
@Slf4j(topic = "c.TestPark")
public class TestPark {
public static void main(String[] args) {
Thread t1 = new Thread(()->{
log.debug("start");
Sleeper.sleep(2);
log.debug("park...");
LockSupport.park();
log.debug("resume");
},"t1");
t1.start();
Sleeper.sleep(1);
log.debug("unPark...");
LockSupport.unpark(t1);
}
}
16:31:15.981 c.TestPark [t1] - start
16:31:16.985 c.TestPark [main] - unPark...
16:31:17.987 c.TestPark [t1] - park...
16:31:17.987 c.TestPark [t1] - resume
unPark 可以在 park 之前以及之后进行调用,最后的结果都是导致线程被唤醒,继续执行下去;恢复线程的执行;
park unpark 原理
线程状态切换
关于 WAITING TIMED_WAITED BLOCKED 的理解
BLOCKED 的线程处于等待持有锁的线程放弃锁,自己然后竞争锁的状态;
WAITING 的线程处于本来持有锁,但是执行条件不满足,进入了Monitor 的 WaitSet 队列中等待的线程,使用 notify 唤醒之后,唤醒之后不是意味着直接获取到锁需要前往EntrySet 里面竞争锁
,没有拿到锁之前处于 BLOCKED 状态;
线程有时间限制的等待,之后会被唤醒,唤醒之后不是意味着直接获取到锁需要前往EntrySet 里面竞争锁
,没有拿到锁之前处于 BLOCKED 状态;
状态切换的详细过程理解(对于以前知识的补充):
Java 层面分为线程的 6 种状态转换;
1 NEW -> RUNNABLE
NEW :还没有调用 start() 方法,还没有和操作系统的底层进行关联;
1 调用 NEW线程,与操作系统连接,开始运行 NEW 出来的线程;
2 RUNNABLE -> WAITING
Obj.wait() 方法之后,进入了WaitSet ;
想要唤醒需要有三种方式,不同的方式下面唤醒之后,需要对其进行详细的说明;
Obj.notify();
Obj.notifyAll();
t.interrupt();
第二种情况是:获得了对象锁,掌握了 Owner ,这个时候调用了Obj. wait()
方法会使得线程进入 WaitSet 队列中进行 WAITING 状态;
当调用了Obj.notify() ,Obj.nitifuAll(), t.intertupt(也可以将线程从WaitSet中唤醒到 BLOCKED状态)
的时候;
Obj.notify()
唤醒WaitSet 中一个的线程进入EntryList;
进入 BLOCKED 状态之后,需要等待线程释放了 Owner 锁的时候,BLOCKED里面的线程才能都被唤醒去竞争锁;
竞争到了:BLOCKED -> RUNNABLE
竞争失败:保持 BLOCKED
其余的全部 WAITING
Obj.notifyAll()
唤醒WaitSet 中全部的线程进入EntryList;
进入 BLOCKED 状态之后,需要等待线程释放了 Owner 锁的时候,BLOCKED里面的线程才能都被唤醒去竞争锁;
竞争到了:BLOCKED -> RUNNABLE
竞争失败:保持 BLOCKED
其余的全部 BLOCKED
t.intertupt
唤醒 WaitSet 中全部的线程进入EntryList;
进入 BLOCKED 状态之后,需要等待线程释放了 Owner 锁的时候,BLOCKED里面的线程才能都被唤醒去竞争锁;
竞争到了:BLOCKED -> RUNNABLE
竞争失败:保持 BLOCKED
其余的全部 BLOCKED
3 RUNNABLE -> WAITING
线程调用使用 t.join() 方法,当前线程从 RUNNABLE -> WAITING
这个里面涉及到两个线程,一个可以理解为主线程,一个为需要加入主线程的线程;
是主线程在等待 t 线程的结束,不是 t 线程等待主线程的结束,需要清楚主次关系;
是调用方法这个线程进入的等待,t.join() ;主线程进入等待。等待需要等待的线程运行结束;
当前线程在 t 线程对象的监视器上等待;
主线程会从RUNNABLE -> WAITING状态;
t 线程运行结束或者,调用了当前线程的 interrupt() 方法之后,当前线程从 WAITING -> RUNNABLE
4 RUNNABLE -> WAITING
当前线程调用 LockSupport.park() 方法会使得当前线程从 RUNNABLE->WAITING
调用 LockSupport(目标线程) 或者调用了当前线程的 interrupt() 会让目标线程从 WAITING -> RUNNABLE 状态;
活跃性
为了增加并发度,可以将锁的粒度减小,使得线程之间的访问互不相干,但是同时,带来了可能发生死锁的情况;
活跃性:一个线程的代码不多,但是由于某种原因一直执行不结束;
活跃性包含下面的三种情况:
死锁
设计对象锁的时候,为了提高并发度,可能设计的锁的粒度比较小,但是同时可能造成死锁的情况;
所谓的死锁可以使用下面的描述:
t1 线程想要获得 A 对象的锁,接下来想要获取 B 对象的锁;
t2 线程想要获取 B 对象的锁,接下来想要获取 A 对象的锁;
@Slf4j(topic = "c.TestDeadLock")
public class TestDeadLock {
public static void main(String[] args) {
test1();
}
private static void test1() {
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(() -> {
synchronized (A) {
log.debug("lock A");
sleep(1);
synchronized (B) {
log.debug("lock B");
log.debug("操作...");
}
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (B) {
log.debug("lock B");
sleep(0.5);
synchronized (A) {
log.debug("lock A");
log.debug("操作...");
}
}
}, "t2");
t1.start();
t2.start();
}
}
上面的代码中:
刚开始 t1 线程锁住了 A 后面想要锁住 B;
刚开始 t2 线程锁住了 B 后面想要锁住 A;
两个都想要访问已经被控制的锁,造成了死锁的现象;
定位死锁 进行处理
使用的检测工具:
1、jconsole;
在这个里面显示死锁信息以及出错的代码位置;
2、jps 定位 线程 id ,再用 jstack 定位死锁;
(base) yimeng@YIMENGdeMacBook-Pro case_java8 % jps
5697 Jps
5380 RemoteMavenServer36
5196 Launcher
572
5677 Launcher
5678 TestDeadLock
(base) yimeng@YIMENGdeMacBook-Pro case_java8 % jstack 5678
2022-04-01 20:31:22
Full thread dump OpenJDK 64-Bit Server VM (25.312-b07 mixed mode):
"Attach Listener" #14 daemon prio=9 os_prio=31 tid=0x000000011a2be800 nid=0x5603 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"DestroyJavaVM" #13 prio=5 os_prio=31 tid=0x0000000129103800 nid=0x1603 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"t2" #12 prio=5 os_prio=31 tid=0x000000012890d000 nid=0xa303 waiting for monitor entry [0x00000001706a2000]
java.lang.Thread.State: BLOCKED (on object monitor)
at cn.itcast.n4.deadlock.TestDeadLock.lambda$test1$1(TestDeadLock.java:32)
- waiting to lock <0x000000076baa6478> (a java.lang.Object)
- locked <0x000000076baa6488> (a java.lang.Object)
at cn.itcast.n4.deadlock.TestDeadLock$$Lambda$2/431687835.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"t1" #11 prio=5 os_prio=31 tid=0x00000001289da800 nid=0xa503 waiting for monitor entry [0x0000000170496000]
java.lang.Thread.State: BLOCKED (on object monitor)
at cn.itcast.n4.deadlock.TestDeadLock.lambda$test1$0(TestDeadLock.java:21)
- waiting to lock <0x000000076baa6488> (a java.lang.Object)
- locked <0x000000076baa6478> (a java.lang.Object)
at cn.itcast.n4.deadlock.TestDeadLock$$Lambda$1/1510067370.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"Service Thread" #10 daemon prio=9 os_prio=31 tid=0x000000011a0dc800 nid=0xa903 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C1 CompilerThread3" #9 daemon prio=9 os_prio=31 tid=0x000000011d012800 nid=0x3d03 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread2" #8 daemon prio=9 os_prio=31 tid=0x000000011d809000 nid=0x3f03 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread1" #7 daemon prio=9 os_prio=31 tid=0x000000011d808800 nid=0x4003 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread0" #6 daemon prio=9 os_prio=31 tid=0x000000012a068800 nid=0x3b03 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Monitor Ctrl-Break" #5 daemon prio=5 os_prio=31 tid=0x000000011a0d7800 nid=0x3a03 runnable [0x000000016f642000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
- locked <0x000000076adc4e98> (a java.io.InputStreamReader)
at java.io.InputStreamReader.read(InputStreamReader.java:184)
at java.io.BufferedReader.fill(BufferedReader.java:161)
at java.io.BufferedReader.readLine(BufferedReader.java:324)
- locked <0x000000076adc4e98> (a java.io.InputStreamReader)
at java.io.BufferedReader.readLine(BufferedReader.java:389)
at com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:49)
"Signal Dispatcher" #4 daemon prio=9 os_prio=31 tid=0x0000000128835000 nid=0x3903 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Finalizer" #3 daemon prio=8 os_prio=31 tid=0x000000011a02a800 nid=0x3303 in Object.wait() [0x000000016f112000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x000000076ab08ee0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:144)
- locked <0x000000076ab08ee0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:165)
at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:216)
"Reference Handler" #2 daemon prio=10 os_prio=31 tid=0x000000012a01d000 nid=0x4d03 in Object.wait() [0x000000016ef06000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x000000076ab06c00> (a java.lang.ref.Reference$Lock)
at java.lang.Object.wait(Object.java:502)
at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
- locked <0x000000076ab06c00> (a java.lang.ref.Reference$Lock)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
"VM Thread" os_prio=31 tid=0x000000012a018800 nid=0x4f03 runnable
"GC task thread#0 (ParallelGC)" os_prio=31 tid=0x0000000129014000 nid=0x2007 runnable
"GC task thread#1 (ParallelGC)" os_prio=31 tid=0x0000000128812000 nid=0x1e03 runnable
"GC task thread#2 (ParallelGC)" os_prio=31 tid=0x0000000128813000 nid=0x5403 runnable
"GC task thread#3 (ParallelGC)" os_prio=31 tid=0x000000012881c000 nid=0x5303 runnable
"GC task thread#4 (ParallelGC)" os_prio=31 tid=0x0000000129015000 nid=0x2d03 runnable
"GC task thread#5 (ParallelGC)" os_prio=31 tid=0x0000000129015800 nid=0x5203 runnable
"GC task thread#6 (ParallelGC)" os_prio=31 tid=0x000000012881d000 nid=0x2f03 runnable
"GC task thread#7 (ParallelGC)" os_prio=31 tid=0x000000012881d800 nid=0x3103 runnable
"VM Periodic Task Thread" os_prio=31 tid=0x000000012883d800 nid=0xa703 waiting on condition
JNI global references: 319
Found one Java-level deadlock:
=============================
"t2":
waiting to lock monitor 0x000000012882f640 (object 0x000000076baa6478, a java.lang.Object),
which is held by "t1"
"t1":
waiting to lock monitor 0x0000000128831480 (object 0x000000076baa6488, a java.lang.Object),
which is held by "t2"
Java stack information for the threads listed above:
===================================================
"t2":
at cn.itcast.n4.deadlock.TestDeadLock.lambda$test1$1(TestDeadLock.java:32)
- waiting to lock <0x000000076baa6478> (a java.lang.Object)
- locked <0x000000076baa6488> (a java.lang.Object)
at cn.itcast.n4.deadlock.TestDeadLock$$Lambda$2/431687835.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
"t1":
at cn.itcast.n4.deadlock.TestDeadLock.lambda$test1$0(TestDeadLock.java:21)
- waiting to lock <0x000000076baa6488> (a java.lang.Object)
- locked <0x000000076baa6478> (a java.lang.Object)
at cn.itcast.n4.deadlock.TestDeadLock$$Lambda$1/1510067370.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
Found 1 deadlock.
(base) yimeng@YIMENGdeMacBook-Pro case_java8 %
哲学家就餐问题 目前只能看到死锁的现象
public class TestDeadLock {
public static void main(String[] args) {
Chopstick c1 = new Chopstick("1");
Chopstick c2 = new Chopstick("2");
Chopstick c3 = new Chopstick("3");
Chopstick c4 = new Chopstick("4");
Chopstick c5 = new Chopstick("5");
new Philosopher("苏格拉底", c1, c2).start();
new Philosopher("柏拉图", c2, c3).start();
new Philosopher("亚里士多德", c3, c4).start();
new Philosopher("赫拉克利特", c4, c5).start();
new Philosopher("阿基米德", c1, c5).start();
}
}
@Slf4j(topic = "c.Philosopher")
class Philosopher extends Thread {
Chopstick left;
Chopstick right;
public Philosopher(String name, Chopstick left, Chopstick right) {
super(name);
this.left = left;
this.right = right;
}
@Override
public void run() {
while (true) {
// 尝试获得左手筷子
synchronized (left) {
// 尝试获得右手筷子
synchronized (right) {
eat();
}
}
}
}
Random random = new Random();
private void eat() {
log.debug("eating...");
Sleeper.sleep(0.5);
}
}
class Chopstick {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "筷子{" + name + '}';
}
}
一个哲学家拿到了一个筷子,但是想要吃饭,得等到旁边的一个人放下去筷子,旁边的人有不想放下去,都在互相等待,造成了死锁;都想占用着资源最终谁也获取不到;
活锁
所谓的活锁就是:两个线程互相改变对方的结束条件,导致最后谁也无法结束;
导致互相停不下来,死锁是两个线程都运行不下去;
一个是停不下来,一个是运行不下去,是有一定的区别的;
@Slf4j(topic = "c.TestLiveLock")
public class TestLiveLock {
static volatile int count = 10;
static final Object lock = new Object();
public static void main(String[] args) {
new Thread(() -> {
// 期望减到 0 退出循环
while (count > 0) {
sleep(0.2);
count--;
log.debug("count: {}", count);
}
}, "t1").start();
new Thread(() -> {
// 期望超过 20 退出循环
while (count < 20) {
sleep(0.2);
count++;
log.debug("count: {}", count);
}
}, "t2").start();
}
}
// 上面的代码在运行的过程中,都想要停止下来,但是两个因为彼此之间相互修改了彼此的停止条件,导致两个线程都无法停下来;
解决活锁的时候,可以是他们的睡眠之间进行错开,就会避免相互改变结束时间,导致的都停不下来;
饥饿锁
当前线程的优先级别太低,导致始终没有办法执行自己的线程;
ReentryLock
可重入
@Slf4j(topic = "c.ReentryLock")
public class TestReentryLock {
private static ReentrantLock lock = new ReentrantLock();
// lock = 普通对象 + Monitor
public static void main(String[] args) {
lock.lock();
try {
log.debug("enter main");
m1();
} finally {
lock.unlock();
}
}
public static void m1() {
lock.lock();
try {
log.debug("enter m1");
m2();
} finally {
lock.unlock();
}
}
public static void m2() {
lock.lock();
try {
log.debug("enter m2");
} finally {
lock.unlock();
}
}
}
可打断
锁超时
放置锁无限制的等待下去,超过一定时间就放弃掉;
tryLock();
可以解决哲学家就餐问题,以前使用 synchronized 拿不到锁之后,就会一直等待中,放置一直等待,使用 tryLock 互相谦让一下可以都拿到自己需要的资源;
公平锁
ReentryLock 默认是不公平锁的;
想要使用公平锁,谁先来,谁先使用需要进行配置;
tryLock 不一定使用公平锁,一般不会使用公平锁;
条件变量
ReentryLock 值的是可以使用多个条件变量,可以有针对性的唤醒;
阶段小结:
同步与互斥之间的区别:
同步:某一个条件不满足的时候,某一个线程进入等待的状态
,在条件满足之后可以继续运行;
互斥:保持临界区代码的原子性
,
Monitor 就是管程,翻译的情况有所不同;互斥以及同步;
JMM Java 内存模型
JMM 分为主存以及工作内存;
可见性
在主存中修改了属性之后,内存中的其他线程能不能马上知道什么属性被修改了,修改成为了什么属性;
@Slf4j(topic = "c.Test32")
public class Test32 {
static boolean run = true;
public static void main(String[] args) {
Thread t = new Thread(()->{
while (run) {
}
});
t.start();
Sleeper.sleep(1);
log.debug("停止 t 线程");
run = false;
}
}
// 上面的代码在执行的结果中,虽然在 main 方法中将 run 变成了 false 但是 t1 线程并没有停止下来,下面进行详细的分析;
解决这和可见性的问题:
使用 volatile ;
加上去 volatile 之后,就每次让去主存中读取变量,不是在自己的工作内存中读取变量,因为这个变量是可以变化的,要考虑到变化,所以需要去主存中读取 volatile 修饰的变量;
volatile 以及 symchornized 都是可以保证静态变量以及成员变量的可见性的;但是 synchornized 在保证了可见性的时候,会创建 Monitor 对象,相比较而言 volatile 是比较轻量级的,建议使用;
volatile 只能保证可见性不能保证原子性;
适合一个线程写,其他线程读取的操作;
有序性
JVM 会在保证程序的正确性的前提下,可以调整语句执行的顺序;,因为在语句的执行被调整之后,会使得运行效率变得高一些,但是在多线程的运行之下,指令重排有时候会导致运行出其他不正确的结果;
上面的指令的顺序发生了变化叫做指令重排,在单线程下面,指令重排是没有什么关系的,但是在多线程的访问之下,会存在一定的问题;会影响执行结果的正确性;
为了实现指令的并行处理,提成处理速度;
指令重排的禁用
volatile 禁止代码的重排序,禁止变量的运行重排序;可以防止变量之前的代码重排序;加了一个写屏障在底层的原理中;保证它之前的所有代码不会排到它的前面执行了;
vilatile 实现原理
内存屏障:
内存屏障是怎么保证可见性以及有序性呢?下面是解释:
可见性
- 写屏障之前的代码的执行结果会自动的同步到主存当中;不仅仅是 volatile 修饰的变量进行主存的同步,他之前的变量都会进行同步到主存的操作;
- 读屏障保存的是在屏障之后对于共享变量的读取是主存中最新的数据;这样子就保证了数据的可见性;
一个解决了数据更新到主存中,一个确保了读取的数据每次都是最新的,这样子保证了数据的可见性;
上面描述了使用读写屏障保证数据的可见性的原理;
上面保证了多线程访问过程中的可见性,下面讨论有序性:
写屏障还可以保证之前的代码不会发生指令重排;不会将写屏障之前的代码出现在写屏障之后;
读屏障保证了它后面的代码是不会被指令重排序的,这样子保证了,禁用指令重排序,使得对于数据的操作是安全的;
volatile 只能保证可见性以及有序性,不能保证原子性;
只能保证本线程的不能发生指令重排序;多线程之间是没有办法控制的;
happen - before 规则
对共享变量的写操作,以及其他线程的读操作是可以见到的,是可见性以及有序性的一套规则的总结;没有这个规则,难以实现一个线程对于共享变量的写,以及实现对其他线程对于共享变量的读操作;
因为 synchronized 的添加,使得可见性保证了;
volatile 的使用一个是为了一个读操作线程,其他写操作线程的时候,执行;
还有就是需要保证可见性以及不要发生指令重排时候使用,其他的时间,应该使用 synchronized 的时候就使用;
阶段小结
乐观锁
在前面的学习中,使用了加锁的方式实现了多线程访问,下面使用不加锁的方式,实现多线程的访问是安全的;
CAS 与 volatile
在使用代码进行测试的时候,发现没有使用锁,但是可以使用 AtomicInteger 的解决方式,它的具体实现方式如下面所示:
CAS 操作,compare and set(swap)
交换的逻辑就是:从 Account 对象中获取一个余额数值,当前线程按照自己的代码逻辑执行一次操作;到这里会有两个数值,一个是原来的余额数值,一个是自己修改之后的余额数值;此时再次获取 Account 里面的最新的余额数值,发现最新的和以前获取到的是同样的余额数值,那么将修改后的余额数值和原来的数值进行替换,否则 CAS 失败;这样子保证了代码执行的原子性;
采用的不断尝试。一直到最后成功的时候才会结束;
volatile 与 CAS 的联合使用
volatile 可以保证数据的一致性以及操作执行的有序性,没有 volatile 的时候,那么 CAS 每次获取到的数据可能不是最新的,那么执行起来就会出现问题;
为什么无锁的效率更加高?
1、线程的上下文切换对于性能是消耗是比较大的;
2、CAS 在多核 cpu 下面效率是比较高的;
CAS volatile 实现无锁线程并发 是一种乐观锁
进行不断的尝试,不害怕修改,改了自己重试就行;实际上是没有锁的
synchronized 是一种悲观锁的设计思想,放置其他线程进行修改;实际上是存在锁的;解锁之后才能使得其他的线程进行修改;
CAS 无锁并发,无阻塞并发,不阻塞,自己不断尝试即可,如果太竞争激烈,也会使得效率降低;
基于 CAS Java 设计的工具类
原子整数
在底层是使用 CAS 以及 vilatile 保证数据的可见性,原子性,有序性的;
实现了无锁状态下面的线程安全;
调用相关的方法,可以使得 i++ ++i --i i-- 等操作,在操作数据的时候,保证了数据操作的完整性;
同时也存在方法可以使得递增多个数字;
原子引用
保护的数据是引用数据类型;
原子数组
保护数组中的每一个元素
字段更新器
保护对象的属性
原子累加器
对于数字的累加操作;
专门用来做累加的类,效率比较高一些;
Unsafe
非常的底层,一般不使用,使用了之后,可能产生一些错误;
阶段小结
不可变类的使用
@Slf4j(topic = "c.Test1")
public class Test1 {
public static void main(String[] args) {
// 不可变类的使用 JDK 8 新加入的类,不用加锁,就是直接可以执行的,减少了系统的消耗
DateTimeFormatter stf = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
TemporalAccessor parse = stf.parse("1951-04-21");
log.debug("{}", parse);
}).start();
}
}
private static void test() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 10; i++) {
new Thread(() -> {
synchronized (sdf) {// 使用锁解决 效率比较低下
try {
log.debug("{}", sdf.parse("1951-04-21"));
} catch (Exception e) {
log.error("{}", e);
}
}
}).start();
}
}
}
不可变类的设计
以 String 为例,设计不可变的类,保证线程的安全;
1、类上面是 final 的
2、维护 String 的 value 是 final 的 char[] 数组,只能在构造的时候赋值,
3、privite int hash 私有的,外界不能修改,在进行赋值的时候,调用一次 hash 函数,后面就不能再次计算 hash值了;
在 String 上面 privite final char value[]; 这里使用了保护性拷贝的思想;
创建新数组的时候,会进行赋值;
小结:为什么 String 类是不可以变的:
类定义了 final 类不能继承,不能被子类继承修改
成员变量不能修改,final 定义了引用不能变,privite 表示外部无法修改,并且不提供修改的方法,所以是不可变的;
享元模式
字符串常量池的创建,相同取值的对象共享;
享元模式的体现 - 数据池的体现
Long 里面的缓存,常量池;
避免了对象的重复创建;
包装类的享元模式的体现如下:
自己创建一个连接池的小 demo
每次的创建以及关闭数据库都是非常的消耗资源的,可以制作出来一个连接池,使用到了享元模式,可以减少系统的资源消耗;
class Pool {
// 1 指定连接池大小
final private int poolSize;
// 2 连接对象的数组
private Connection[] connections;
// 3 连接状态数组
// 0 表示空闲 1 表示繁忙
private AtomicIntegerArray status;
// 构造方法
public Pool(int poolSize) {
this.poolSize = poolSize;
// 创建的连接池 里面存放连接对象
this.connections = new Connection[poolSize];
// 连接池中每一个连接对象的状态;需要是不加锁的原子性的;
this.status = new AtomicIntegerArray(new int[poolSize]);
for (int i = 0; i < poolSize; i++) {
connections[i] = new MockConnection();
}
}
// 借出去连接
public Connection borrow() {
while (true) {
for (int i = 0; i < poolSize; i++) {
// 查看连接对象的状态,状态是 0 的话,还没有分配借出去即可
if (status.get(i) == 0) {
// 使用 cas 进行操作,将 0 改为 1
if (status.compareAndSet(i, 0, 1)) {
// cas 成功之后才能返回
return connections[i];
}
}
}
// 没有空闲连接的时候 进入等待即可
synchronized (this) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 返回连接
public void free(Connection conn) {
for (int i = 0; i < poolSize; i++) {
if (connections[i] == conn) {
status.set(i,0);
break;
}
}
// 有了连接池,可以唤醒停止等待;
synchronized (this) {
notifyAll();
}
}
}
final 的原理
设置 final 的原理 - 使用写屏障
final 设计的时候 使用到了写屏障的思想;
保证了写屏障之前的指令的执行顺序是不能变化的,写屏障之前的数据的更新会被同步到主存中,实现了数据的更新;对其他线程是可见的;
获取 final 变量的原理
无状态
没有成员变量,也就没有线程安全的问题;