问题思考
有下面这么一段代码,在SynchronizedDemo中存在一个count字段,然后提供一个方法用来给字段的值加1,另一个方法用来获取字段的值。现在有两个线程分别对该字段进行+1操作,最后在两个线程中输出该字段的值。
public class SynchronizedDemo {
private Integer count = 0;
private void add(){
count++;
}
private Integer get(){
return count;
}
public static void main(String[] args) throws InterruptedException {
SynchronizedDemo demo = new SynchronizedDemo();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
demo.add();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
demo.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(demo.get());
}
}
多运行几次你会发现答案并不是20000,可能有的时候是20000有的时候不到。究其原因就是线程不安全导致的,当多线程同时对同一个变量就行修改时就会导致线程不安全。那有没有什么方式能让他变得线程安全呢?方式有很多种,最简单的方式就是使用「synchronized」关键字修饰add()方法。
synchronized
synchronized关键字是JVM提供的,它的主要作用就是用来线程同步的。synchronized通常可以用来修饰在实例方法、静态方法、同步代码块,synchronized会在JVM内使用锁来提供同步,而不同的修饰锁住的对象也并非一致。
实例方法
当synchronized被用来修饰实例方法时,它锁住的对象就是实例本身。那如何证明呢?
@Slf4j
public class InstanceMethodDemo {
private static int count = 0;
private synchronized void add(){
count++;
}
private int get(){
return count;
}
public static void main(String[] args) throws InterruptedException {
InstanceMethodDemo demo1 = new InstanceMethodDemo();
InstanceMethodDemo demo2 = new InstanceMethodDemo();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
demo1.add();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
demo2.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
log.info("count = {}",demo1.get());
}
}
在上面的代码中,定义一个私有静态变量count,然后提供了实例方法add()用来将值+1,提供get()方法用来获取count的值。重点在add()方法,我们将它使用「synchronized」修饰了。多次运行代码,你会发现最后的值并不是20000。
原因就在于,这里的synchronized锁住的对象是实例,而我们代码中的实例指的是「demo1」和「demo2」,对于demo1和demo2而言,它们根本就不是同一个对象。而被锁的不是同一对象,那当然就没有同步的效果了。
静态方法
当synchronized修饰的是静态方法时,那锁住的对象又是什么呢?
@Slf4j
public class StaticMethodDemo {
private static int count = 0;
public synchronized static void add(){
count++;
}
public int get(){
return count;
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
StaticMethodDemo.add();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
StaticMethodDemo.add();
}
});
Thread t3 = new Thread(() -> {
synchronized (StaticMethodDemo.class){
for (int i = 0; i < 10000; i++) {
count++;
}
}
});
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
log.info("count = {}",count);
}
}
在上面的代码中,一共三个线程对同一个静态变量count进行+1操作,线程t1和t2调用的add方法通过synchronized修饰是线程安全的,而线程三锁住的是「StaticMethodDemo.class」对象,因为三个线程锁住的是同一个对象,这样保证了同一时间只能有一个线程能修改count的值,从而保证了线程安全。这也从侧面说明了,使用synchronized锁修饰静态方法,它锁住的是类对象。
代码块
上面使用synchronized锁住的范围是整个方法,但有时候我们不需要锁这么大的范围,这时候我们就可以通过synchronized修饰代码块,从而相对自由的设置锁范围。实例和静态方法被synchronized修饰时锁的对象分别是实例和类,而代码块锁住的就是指定的对象。
@Slf4j
public class CodeBlockDemo {
private final Object lock = new Object();
private int count = 0;
public void add(){
synchronized (lock){
count++;
}
}
public int get(){
return count;
}
public static void main(String[] args) throws InterruptedException {
CodeBlockDemo demo = new CodeBlockDemo();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
demo.add();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
demo.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
log.info("count = {}",demo.get());
}
}
可重入和不可中断
上面介绍了synchronized的作用,同时它还有两个特性需要了解。
可重入性
可重入性是指对于同一线程可以多次进入synchronized代码块获取的是同一把锁。
@Slf4j
public class ReenterFeatureDemo {
private static final Object LOCK = new Object();
public static void main(String[] args) {
synchronized (LOCK){
log.info("第一次进入同步代码块");
synchronized (LOCK){
log.info("再一次进入同步代码块");
}
}
}
}
上面示例的运行结果是:
❝
第一次进入同步代码块
再一次进入同步代码块❞
从上面的结果可以看出,主线程第一次进入synchronized同步代码块中,接着并没有释放锁(没有离开同步代码块)再次进入synchronized代码块,而且这两次同步代码块所有都是统一对象「LOCK」。正是通过这一点证实了synchronized可重入的特性,这个与ReentrantLock很像,它们都支持可重入,这个特性可以让我们更容易避免死锁。
不可中断性
什么是不可中断性呢?简单的说就是当一个线程获取到锁之后,如果该线程没有退出同步代码块(也就是没有释放锁),而其他线程想要获取该锁是无法直接获取到的。其他线程想要获取到锁必须等持有锁对象的线程释放锁之后才能获取到,其他线程是没有权利剥夺当前对象所持有的锁。
@Slf4j
public class UninterruptibleDemo {
private static final Object LOCK = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
synchronized (LOCK) {
try {
log.info("线程[{}]进入同步代码块",Thread.currentThread().getName());
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(() -> {
synchronized (LOCK) {
try {
log.info("线程[{}]进入同步代码块",Thread.currentThread().getName());
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
log.info("程序退出");
}
}
上面的代码中,我们创建两个线程,线程内部使用synchronized锁住同一对象,运行结果如下所示:
❝
13:54:43.494 [Thread-0] INFO com.buydeem.share.sync.UninterruptibleDemo – 线程[Thread-0]进入同步代码块
13:54:53.498 [Thread-1] INFO com.buydeem.share.sync.UninterruptibleDemo – 线程[Thread-1]进入同步代码块
13:55:03.499 [main] INFO com.buydeem.share.sync.UninterruptibleDemo – 程序退出❞
从上面的结果可以看出,当线程「Thread-0」获取到对象锁时,「Thread-1」只能进入等待,在「Thread-0」释放锁后(即退出同步代码块),线程「Thread-1」才获取到了锁进入了同步代码块。这就是不可中断的特性。
原文始发于微信公众号(一只菜鸟程序员):java多线程之synchronized关键字
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/73065.html