文章目录
- synchronized锁定的资源
- 锁定对象改变
- 锁定对象为字符串常量
- 减小锁的颗粒度
- 脏读问题
- 支持重入
- 重入1
- 重入2
- synchronized 与 异常处理
- synchronized的可见性问题
- 原子性操作
- 一道的面试题
- 方式1:
- 方式2:
- 方式3:
synchronized锁定的资源
synchronized
修饰的是方法或者代码块来实现同步,但其实锁定的资源其实是对象。synchronized
修饰于3种方式(静态方法、普通方法、方法块),其锁定的资源有2种(类对象、类的实例)加synchronized关键字之后不一定能实现线程安全,具体还要看锁定的对象是否唯一。
-
synchronized
修饰静态方法,锁定的资源是Class对象(类对象,Class class = x x x.Class)
两种方式:
public class Demo {
private static int count = 10;
//synchronize关键字修饰静态方法锁定的是类对象
//方式1
public synchronized static void test(){
//临界区
count--;
}
//方式二
public static void test2(){
synchronized (Demo4.class){//这里不能替换成this
//临界区
count--;
}
}
}
-
synchronized
修饰普通方法,锁定的资源是类的实例(User o = new User())
三种方式:
- 对象用new的方式
private int count = 10;
private Demo reentryDemo = new Demo();
public void test(){
synchronized (object){
//临界区
count--;
}
}
- 用this来代替,当前对象的实例
public void test() {
synchronize (this) {}
}
- 直接修饰在普通方法上
public synchronized void test() {}
-
synchronized
修饰代码块/方法块,其实实际上也是方法的一部分, 所以锁定的资源也是根据方法来。
锁定对象改变
- 如果锁定对象的属性发生改变,不会影响锁的使用。
- 如果锁定对象修改为另外一个对象,这种锁定对象的改变了,等于是一把新锁了。
示例代码:
public class Demo {
public Lock lock = new Lock();
public void test () {
synchronized (lock) {
while (true) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("hello word");
}
}
}
public static void main(String[] args) throws InterruptedException {
Demo reentryDemo = new Demo();
new Thread(reentryDemo::test,"t1").start();
TimeUnit.MILLISECONDS.sleep(10);
// reentryDemo.lock.i=20;
reentryDemo.lock = new Lock();
new Thread(reentryDemo::test,"t2").start();
}
}
class Lock {
public int i = 0;
}
锁定对象为字符串常量
不要以字符串常量作为锁定的对象,在jvm中两个相同的字符串都是指向常量池同一块内存。
所以虽然名字不同,但实际上还是同一把锁。
public String sLock1 = "lock";
public String sLock2 = "lock";
public void test1 () {
synchronized (sLock1) {
while (true) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("hello word");
}
}
}
public void test2() {
synchronized (sLock2) {
log.debug("test2 end");
}
}
另外一种情况,不属于并发的知识点了。这种就是2个对象了。
public String sLock1 = new String("lock");
public String sLock2 = new String("lock");
减小锁的颗粒度
通过减少同步的代码块,降低锁的颗粒度。同步的代码块越少越好,从而提高效率。
比如说在业务上只要保证count的同步,可以采用细颗粒度的锁,从而减少线程竞争的时间。
优化方案:就只需要对count++局部上锁,不用对整个方法上锁。
Bad:
int count = 0;
public synchronized void test(){
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
count ++;
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Good:局部上锁
public void test2(){
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (this) {
count ++;
}
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
脏读问题
一般对于set方法需要进行上锁来解决线程安全问题,但是get呢?因为get不会使对象产生线程安全问题,只是会出现脏读问题。
对于脏读的解决应该具体分析具体讨论,因为加锁其实会使代码效率变低。
实际业务当中应该看是否允许脏读,不允许的情况下对读方法也要加锁。
支持重入
重入1
Q:如果一个同步方法调用另一个同步方法,另一个同步能否被调用呢?
A:synchronized
默认是支持重入的,所以能被调用成功。
Q:为何需要给另一个方法也上锁呢?
A:是保证另一个方法也会被单独调用的时候,是线程安全的。
重入2
Q:父类的同步方法,子类进行重写,重写方法也调用了父类的同步方法,能否被调用?
A:也是可以的。这种也是重入锁的应用之一。
code:
@Slf4j(topic = "console")
public class ReentryDemo {
//一个同步方法调用另外一个同步方法,能否获得锁?
public synchronized void test1() {
log.debug("test1 start.........");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//调用另一个同步方法
test2();
log.debug("test1 end.........");
}
public synchronized void test2() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("test2 start & end.");
}
public static void main(String[] args) {
Demo2 reentryDemo = new Demo2();
reentryDemo.test1();
new Thread(reentryDemo::test2).start();
}
}
@Slf4j(topic = "console")
class Demo2 extends ReentryDemo {
//重写父类的同步方法,并调用父类的同步方法
@Override
public synchronized void test1() {
log.debug("sub test1 start.........");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
super.test1();
log.debug("sub test1 end.........");
}
}
synchronized 与 异常处理
当同步方法报异常的时:
- 如果没有对异常处理,锁会释放掉,线程不安全了。
- 如果对异常进行处理了,锁不会被释放。
所以在并发编程中,一定要对异常进行处理。
Code:
@Slf4j(topic = "console")
public class ExceptionDemo {
Object o = new Object();
int count = 0;
void test() {
//
synchronized (o) {
//t1进入并且启动
log.debug("start......");
//t1 会死循环 t1 是不会释放锁的
while (true) {
count++;
log.debug(" count = {}", count);
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//加5次之后 发生异常
/**
* 代码出异常如果不处理则会释放锁
* 如果处理了则不会释放锁
*/
if (count == 5) {
try {
int i = 1 / 0;
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
public static void main(String[] args) throws InterruptedException {
ExceptionDemo demo = new ExceptionDemo();
new Thread(() -> {
demo.test();
}, "t1").start();
TimeUnit.MILLISECONDS.sleep(1);
new Thread(() -> {
demo.test();
}, "t2").start();
}
}
synchronized的可见性问题
多个线程共享一个变量时,会出现可见性问题。
当有线程修改变量的值后,其他线程看不到值的变化,这就是可见性问题。
@Slf4j(topic = "console")
public class Demo {
boolean running = true;
List<String> list = new ArrayList<>();
/**
* t1线程
*/
public void test(){
log.debug("test start...");
while (running){
}
log.debug("test end...");
}
public static void main(String[] args) throws InterruptedException {
Demo demo = new Demo();
new Thread(demo :: test,"t1").start();
TimeUnit.SECONDS.sleep(2);
demo.running = false;
}
}
Q:上面的启动t1线程后,在主线程改变了running
的值,t1线程会停止吗?
A:不会。t1线程一直在死循环。
原因:t1线程和主线程都共用了running变量,该变量会在t1保留一份副本在栈上,这样main修改了running,t1也不知道。这种可见性的问题实际上是指令重排导致的。running这个副本在t1线程中的形式如下(不是真的是这样,只是伪代码,表达大概意思),这样main吧running怎么修改都不会重新读到running了。
boolean flag =running;
if (flag) {
while(true){}
}
避免这种现象发生的做法:
- 在while(running)里添加一些操作,让jvm不敢指令重排。
- 用
volatile
修饰变量running,让使用这个变量的线程都需要到线程去读取。
volatitle这个关键字在C语言常考的,不到寄存器中读取变量的备份,而是每次都要在内存中读取。避免
如果要深究的话参考R大的回答:https://hllvm-group.iteye.com/group/topic/34932
原子性操作
需求:
启动10个线程对count进行1w次++操作,应得结果是10w。
- 用volatile修饰count,因为volatile不能保证原子性,所以所得结果一定是小于10w的。
- 为了保证原子性可以用synchronized上锁,能保证结果准确性,但是上锁的效率较低。
- 利用原子类AtomicInteger。
count++
对于jvm来说需要执行多条指令,而count2.incrementAndGet();
对于jvm来说是执行“一条指令”。
⚠️注意:atomic类连续调用不能构成成原子性
Code:
@Slf4j(topic = "console")
public class Demo {
//volatile只能保证有序性和可见性,不能保证原子性
volatile int count = 0;
public void test(){
for (int i = 0; i < 10000; i++) {
count++;
}
}
//可以通过上锁的方式保证有序性,可见性,和原子性
int count1 = 0;
public synchronized void test1(){
for (int i = 0; i < 10000; i++) {
count1++;
}
}
//可以通过AtomicX变量来保证原子性
AtomicInteger count2 = new AtomicInteger(0);
public void test2(){
for (int i = 0; i < 10000; i++) {
count2.incrementAndGet();
}
}
//如果我们只想执行到1000就不加了,这样能保证原子性吗,结果会是1k吗?
//对原子类多次调用其实不能保证原子性的。
// count2.get能保证原子性,count2.incrementAndGet也能保证原子性
//但是他们2个不能保证原子性。
public void test3(){
for (int i = 0; i < 10000; i++) {
if (count2.get()<1000) {
count2.incrementAndGet();
}
}
}
public static void main(String[] args) {
Demo demo = new Demo();
List<Thread> threads = new ArrayList();
//new 10个线程
for (int i = 0; i < 10; i++) {
threads.add(new Thread(demo::test2, "t-" + i));
}
//遍历这个10个线程 依次启动,都同时对count进行一万次++
threads.forEach((o)->o.start());
//等待10个线程执行完
threads.forEach((o)->{
try {
o.join();
} catch (Exception e) {
e.printStackTrace();
}
});
/*
结果是 小于等于10万的,因为不能保证原子性
*/
log.debug(demo.count+"");
/*
通过上锁的方式效率会比较低
*/
log.debug(demo.count1+"");
/*
使用AtomicX保证原子性
*/
log.debug(demo.count2+"");
}
}
一道的面试题
* 需求:
* 实现一个容器,提供两个方法,add,size
* 写两个线程,线程1添加10个元素到容器中,线程2实现监控元素的个数,
* 当个数到5个时,线程2给出提示并结束线程2
思路:t1线程进行元素添加到容器里,t2线程满足条件后结束。
方式1:
- 思路:t2在while(true)里轮询容器大小
- 注意点:
- List对于2个线程存在不可见性,所以会导致线程2不会结束。这种不可见和while(true)那里一样,是指令重排导致的。
- volatile修饰List后,线程2才可以正确获取容器大小,才在满足条件时会结束
@Slf4j(topic = "console")
public class Container {
// private List lists = new ArrayList();
private volatile List lists = new ArrayList();
public void add(Object o){
lists.add(o);
}
public int size(){
return lists.size();
}
public static void main(String[] args) {
Container container = new Container();
new Thread(() -> {
for (int i = 0; i < 10; i++) {
container.add(new Object());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("size = {}",container.size());
}
log.debug("end");
},"t1").start();
new Thread(() -> {
while (true) {
if (container.size() == 5) {
break;
}
}
log.debug("end");
},"t2").start();
}
}
方式2:
- 思路:
对于方式1来说有一个很大的缺点,就是线程2在死循环,一直在running,浪费CPU性能。
可以通过用锁,等待或唤醒来优化性能: - 一个wait,一个notify:
t2启动拿到锁,不满足条件进行wait(睡眠状态),释放锁;
t1拿到锁后,当满足条件的时候唤醒t2,但没有释放锁,所以t2处于阻塞状态,等到t1执行完毕结束后释放锁
t2拿到锁后执行完毕。 - 不断来回wait、notify:
上面还是不满足需求呀,我们要container大小为5的时候t2执行完毕。
所以需要在满足条件唤醒t2,t1把锁释放给t2。
所以我们可以来回的睡眠唤醒来满足需求。
@Slf4j(topic = "console")
public class Container1 {
private List lists = new ArrayList();
public void add(Object o){
lists.add(o);
}
public int size(){
return lists.size();
}
public static void main(String[] args) throws InterruptedException {
Container1 container = new Container1();
//设一个锁
Object lock = new Object();
new Thread(() -> {
log.debug("start");
synchronized (lock) {
if (container.size() != 5) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("end");
//唤醒t1
lock.notify();
}
//释放锁
},"t2").start();
//为了让t2先调度,先拿到锁
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {
log.debug("start");
synchronized (lock){
for (int i = 0; i < 10; i++) {
container.add(new Object());
if (container.size() == 5) {
//唤醒t2
lock.notify();
try {
//t1睡眠,释放锁
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("size = {}", container.size());
}
}
log.debug("end");
},"t1").start();
}
}
方式3:
- 思路:通过CountDownLatch来进行无锁化优化
- 原因:当不涉及同步,只是涉及线程通信的时候,用synchronized加wait,notify就显得太重了
await和countDown来代替锁的wait和notify
CountDownLatch不涉及锁,当count的值为零时当前线程继续运行
相当于是发令枪,运动员线程调用await等待,计数到0开始运行
@Slf4j(topic = "console")
public class Container2 {
private List lists = new ArrayList();
public void add(Object o){
lists.add(o);
}
public int size(){
return lists.size();
}
public static void main(String[] args) throws InterruptedException {
Container2 container = new Container2();
//设一个门栓,只用倒计时1次
CountDownLatch latch = new CountDownLatch(1);
new Thread(() -> {
log.debug("start");
if (container.size() != 5) {
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("end");
},"t2").start();
//为了让t2先调度,先拿到锁
TimeUnit.SECONDS.sleep(1);
new Thread(() -> {
log.debug("start");
for (int i = 0; i < 10; i++) {
container.add(new Object());
if (container.size() == 5) {
latch.countDown();
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("size = {}", container.size());
}
log.debug("end");
},"t1").start();
}
}