0818(034天 线程/进程05 线程锁)
每日一狗(田园犬西瓜瓜)
线程/进程05 线程锁
文章目录
1. 线程锁
1.1 synchronizedu 排他锁
用于实现同步处理保障数据共享的数据安全性。
数据安全问题是由 共享数据 和 共享数据的修改所导致的。
充当锁对象的对象数量要比线程数量要少,要不就达不到锁的效果,线程在执行到这个代码块的时候,需要获取这个锁对象才能去执行这个代码块。
注意:锁对象最小要比线程对象才能有锁的效果,但是要保证数据的一致性还是锁对象还是要唯一,即同一时刻内,只能有一个线程操作共享数据,其他线程无法对其进行操作。
在哪里上锁
- 同步方法:锁对象为当前类对象
public synchronized void add(){
// 会被串行的代码块
}
- 同步静态方法:锁对象为当前类
private synchronized static void add() {
// 会被串行的代码块
}
- 同步代码块:锁对象要自己找一个唯一
synchronized(锁对象){
// 会被串行的代码块
}
建议使用同步代码块的上锁,颗粒度较小,可以尽可能的提升程序的并发性,提高性能。
前尝一下 小小票员
package com.yang1;
public class Test01 {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
new MyThread("" + (i + 1)).start();
}
}
}
class MyThread extends Thread {
private String name;
private static int count;
private static String suo = "suo";
public MyThread(String name) {
this.name = name;
count = 20;
}
@Override
public void run() {
while (count > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (suo) {
if (count > 0) {
System.out.println(name + "窗口迈出了底" + count + "张票");
count--;
} else {
System.out.println("票以买完");
}
}
}
}
}
synchronized在修饰方法时:
-
当前对象中的synchronized修饰的所有的方法互斥,
-
不能添加到构造方法中,
-
拿到锁的线程可以随意执行被锁修饰的所有的方法,但是其他线程就要等待那锁的线程释放锁后去竞争到锁后才能调用被锁修饰的所有方法。
-
线程重入:避免了死锁
- 拿着锁的线程在执行的时候遇到了需要锁才能执行的方法时,就在申请一下走一下流程,拿到锁后执行完毕在返回给上面那个方法用(左手倒右手)
- 锁方法内部程序允许二次申请当前锁方法进来时拿的那个锁。(你拿着一把钥匙进入一个小区,你进去后还拿着这把锁,等到了一个房子,这个房子上的锁跟小区门口的锁一样,你可以直接把这个房子的锁打开)
父线程的资源是父线程的,子线程无权共享
线程本身是没有父子关系的,也不存在所谓的主线程,只不过为了程序的编码方便,我们一般会引入父子线程来提高自身对程序的认知。线程中创建的线程他也是线程,需要锁的时候还是需要去竞争锁的使用权限。
package com.yang1;
public class Test04 {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
S4.add();
}
}
}
class S4 {
private static final String suo = "suo";
private static int i = 0;
public static void add() {
synchronized (suo) {
i++;
System.out.println(i + "外部创建前" + Thread.currentThread());
new Thread() {
@Override
public void run() {
synchronized (suo) {
System.out.println(i + "内部-----" + Thread.currentThread());
}
}
}.start();
System.out.println(i + "外部创建后" + Thread.currentThread());
}
}
}
1.2 引入锁机制解决安全问题
synchronized锁相比较于Lock接口而言,太重量级了,JDK1.6之引入了几款较为轻量级的锁机制。在竞争不严重的情况下,尽可能使用更少的成本实现,随着竞争的加剧,锁的状态会越来越重,单这个步骤是不可逆的。所在更替(下来、上去、销毁下来的)的时候也要成本的。
- 偏向锁
- 一个线程时使用(就记录一下这把锁的权限,谁能来用)
- 轻量级锁,自动升级为本锁
- 两个线程的时候(谁来竞争这把锁的时候,让他盲等一会,再来问我要)
- 在忙等的时候会占用CPU资源
- 重量级锁
- 当一个线程盲等了好几次了,你都不给我锁权限的时候,此时轻量级锁会升级为重量级锁
在很多情况下,虽然这个对象是线程安全的,但是很多情况下咱们用着线程安全的对象,但是用不到那么安全的对象(竞争没有那么激烈)。
1、一个线程来用的synchronized方法的时候:先加了一个偏向锁,单线程时,锁只会有一个标志(线程PID),用以存储这个锁的拥有者,除此以外,其他对象无法申请该锁(这个标志他会存储到 对象头的标识字mark word中)
2、当第二个线程来的时候他会升级成轻量级锁,这个锁在申请后在被申请的时候,竞争者会去做一个忙等操作(忙等不会释放CPU资源),用于占着CPU资源,锁的拥有者会去将偏向锁更替为轻量级锁。
3、在线程忙的的次数太高(达到某一阈值)的时候,轻量级锁会升级成重量级锁。(当线程之间还是交替进入临界区时,此时还是有序的,轻量级锁还凑合能用。等到多个线程同进入临界区的时候的次数太多了(忙等次数太多),这个锁就会升级成重量级锁)
-
乐观锁:
- cas(compare and set)
- 写入前判定这个值改没改,改了我就在把值拿回去在跑一圈再来写回
-
悲观锁:成本太高了
- 直接将数据锁住,谁都不能改
锁对比
锁 | 描述 | 优点 | 缺点 | 应用场景 |
---|---|---|---|---|
偏向锁 | 线程在大多数情况下并不存在竞争条件,使用同步会消耗性能,而偏向锁是对锁的优化,可以消除同步,提升性能。当一个线程获得锁,会将对象头的锁标志位设为01,进入偏向模式。偏向锁可以在让一个线程一直持有锁,在其他线程需要竞争锁的时候,锁会转变为轻量级锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步块的场景 |
轻量级锁 | 当线程A获得偏向锁后,线程B进入竞争状态,需要获得线程A持有的锁,那么线程A撤销偏向锁,进入无锁状态。线程A和线程B交替进入临界区,偏向锁无法满足,膨胀到轻量级锁,锁标志位设为00 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到所竞争的线程,使用忙等会消耗CPU | 追求响应速度,同步块执行速度非常块 |
重量级锁 | 当多线程交替进入临界区,轻量级锁hold得住。但如果多个线程同时进入临界区,hold不住了,膨胀到重量级锁 | 线程竞争不使用忙等,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,同步块执行速度较慢 |
1.3 重量级锁的附加代价
重量级锁:需要存储许多额外的数据和额外的CPU执行资源
- 执行代价
- 等待队列中还得要看谁时间到了要把谁唤醒
- 在上锁和释放锁的时候也要修改锁的状态
- 存储代价
- 谁拥有锁
- 记录正在竞争的人,
- 维护使用wait进入当前锁的等待队列
2. 对象的数据存储问题
2.1 对象的数据存储问题
对象头:一般包含
- 标识字mark word:存储对象运行时相关数据(默认存储对象的HashCode,分代年龄和锁标志位信息。也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化)
- 类型指针klass point:那个类在方法区中的存储位置
实例数据:
- 具体对象的成员数据,一般按照4字节为单位进行数据存储
对齐字节:
- Java是按照8字节来存储数据对象(我读取数据时就不用猜测他会在那个字节结束了,全读进来,没用的删掉)
- 对齐字节将对象的存储数据凑够8字节的整数倍数。
扩展小芝士
- 被多个线程共享访问的数据称为临界资源
- 临界区:哪一个锁限制了一个代码块,这个代码块就叫临界区
- Java中类其实也是一个对象
- 垃圾回收:将有用的对象拷贝到其他的地方,这一片被拷贝后的原始数据就没用了
- 对象的序列化ID如果不指定他会用当前参与序列化的属性方法求出来一个哈希值来充当序列化ID