文章目录
1. 概述
本篇博客记录synchronized的使用,注意的事项。
2. synchronized关键字锁的是什么?
synchronized关键字锁定的是对象不是代码块,Demo1中锁的是Object对象的实例。
锁定的对象有两种:1.类的实例 2.类对象(类锁)。
加synchronized关键字之后不一定能实现线程安全,具体还要看锁定的对象是否唯一。
下面举几个例子来说明
@Slf4j(topic = "s")
public class Demo1 {
private int count = 10;
private Object object = new Object();
public void test(){
synchronized (object){
//临界区
count--;
log.debug("count = " + count);
}
}
}
@Slf4j(topic = "s")
public class Demo2 {
private int count = 10;
public void test(){
/**
* synchronized(this)锁定的是当前类的实例,这里锁定的是Demo2类的实例
*/
synchronized (this){
count--;
log.debug("count = " + count);
}
}
}
demo1和demo2 锁的都是实例对象,区别在于demo1 锁的是调用test方法的对象内的一个属性object对象。
而demo2锁的是调用test方法的对象本身。注意只有非静态方法才可以用demo2的写法。
@Slf4j(topic = "s")
public class Demo3 {
private int count = 10;
//直接加在方法声明上,相当于是synchronized(this)
public synchronized void test(){
count--;
log.debug("count = " + count);
}
}
Demo3 直接将 synchronized作为方法的修饰符,效果相当于demo2。
但是区别是直接加在方法上,该方法可以是静态方法,若是静态方法的话,那么它锁的其实就是类对象,也就是类锁。例如下面的Demo4
@Slf4j(topic = "s")
public class Demo4 {
private static int count = 10;
//synchronized 关键字修饰静态方法锁定的是类的对象
public synchronized static void test(){
count--;
log.debug("count =" + count);
}
//与上面的效果相同
public static void test2(){
synchronized (Demo4.class){//这里不能替换成this
count--;
}
}
}
3.锁对象的属性改变和锁对象改变对于锁的使用的影响
锁定某对象o,如果o的属性发生改变,不影响锁的使用。但是如果o变成另外一个对象,则锁定的对象发生改变,此时就会影响锁的使用。应该避免将锁定对象的引用变成另外一个对象。
下面举个例子
@Slf4j(topic = "s")
public class Demo1 {
O o = new O();
public void test(){
synchronized (o) {
//这里无限执行
while (true) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("xxxxxxx");
}
}
}
public static void main(String[] args) {
Demo1 demo = new Demo1();
new Thread(demo :: test, "t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread t2 = new Thread(demo :: test, "t2");
//锁对象改变
// demo.o = new O();
//锁对象的属性改变
demo.o.num = 1;
//t2能否执行?
t2.start();
}
public static class O{
int num = 0;
}
}
程序比较简单,线程t1 先启动,获得到了锁执行打印,线程t2 启动后,无法获取锁,尽管此时修改了锁对象的属性num,对于锁的使用也是没有影响的。控制台输出结果如下:
只有线程t1 在打印东西。
若此时将代码改成如下
@Slf4j(topic = "s")
public class Demo1 {
O o = new O();
public void test(){
synchronized (o) {
//这里无限执行
while (true) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("xxxxxxx");
}
}
}
public static void main(String[] args) {
Demo1 demo = new Demo1();
new Thread(demo :: test, "t1").start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread t2 = new Thread(demo :: test, "t2");
//锁对象改变
demo.o = new O();
//锁对象的属性改变
// demo.o.num = 1;
//t2能否执行?
t2.start();
}
public static class O{
int num = 0;
}
}
此时t1线程和t2线程都在执行打印,因为他们现在锁的对象不是同一个,所以不存在竞争,都可以执行。
4.同步方法和非同步方法是否可以同时调用?
答:可以
例如下面的例子,同步方法获取到锁,并不会影响其他非同步方法的使用。
@Slf4j(topic = "s")
public class Demo {
public synchronized void test1(){
log.debug(" test1 start...");
try {
//睡眠5s 由于还要t2要执行 cpu回去执行t2
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug(" test1 end...");
}
public void test2(){
log.debug(" test2 start...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug(" test2 end...");
}
public static void main(String[] args) {
Demo demo = new Demo();
//正在执行一个同步方法 没有释放锁
new Thread(demo :: test1,"t1").start();
//不影响其他线程执行非同步方法(就算他是一个同步方法,如果锁的不是同一个对象也不影响)
new Thread(demo :: test2,"t2").start();
}
}
控制台输出结果如下:
5.读方法需不需要加synchronized?
在set方法的时候,涉及到了共享资源的修改,所以需要加上synchronized。那读方法需要吗?
读方法是否需要加synchronized 首先取决于,这个方法内 存不存在线程安全的问题。若有线程安全的问题那么就要加锁。
然后再看具体的业务是否允许脏读,若不允许脏读,那么就需要加锁。
例如下面的代码
@Slf4j(topic = "s")
public class Demo {
//卡的持有人 senlin
String name;
//卡上的余额 0
double balance;
public synchronized void set(String name,double balance){
this.name = name;
try {
log.debug("set");
//模拟存钱耗时 银行系统处理
Thread.sleep(2000);
log.debug("set end");
} catch (InterruptedException e) {
e.printStackTrace();
}
this.balance = balance;
}
public double getBalance(String name){
return this.balance;
}
// public synchronized double getBalance(String name){
// return this.balance;
// }
@SneakyThrows
public static void main(String[] args) {
Demo demo = new Demo();
//没有启动
Thread zl = new Thread(() -> {
log.debug("余额-{}", demo.getBalance("senlin"));
try {
TimeUnit.SECONDS.sleep(2);
}catch (Exception e){
e.printStackTrace();
}
log.debug("----余额-{}", demo.getBalance("senlin"));
}, "senlin");
//2s
new Thread(() -> demo.set("senlin", 100.0), "yiyi").start();
TimeUnit.SECONDS.sleep(1);
zl.start();
}
}
代码很简单,yiyi向senlin转了100块钱。与此同时senlin查账户,查了两次,出现了脏读。控制台结果如下:
若当前业务不允许脏读的出现。那么对读方法也需要加锁。
代码改成如下:
@Slf4j(topic = "s")
public class Demo {
//卡的持有人 senlin
String name;
//卡上的余额 0
double balance;
public synchronized void set(String name,double balance){
this.name = name;
try {
log.debug("set");
//模拟存钱耗时 银行系统处理
Thread.sleep(2000);
log.debug("set end");
} catch (InterruptedException e) {
e.printStackTrace();
}
/**
*
*/
this.balance = balance;
}
// public double getBalance(String name){
// return this.balance;
// }
public synchronized double getBalance(String name){
return this.balance;
}
@SneakyThrows
public static void main(String[] args) {
Demo demo = new Demo();
//没有启动
Thread zl = new Thread(() -> {
log.debug("余额-{}", demo.getBalance("senlin"));
try {
TimeUnit.SECONDS.sleep(2);
}catch (Exception e){
e.printStackTrace();
}
log.debug("----余额-{}", demo.getBalance("senlin"));
}, "senlin");
//2s
new Thread(() -> demo.set("senlin", 100.0), "yiyi").start();
TimeUnit.SECONDS.sleep(1);
zl.start();
}
}
此时的就解决了脏读的问题,控制台输出结果如下:
6. synchronized是否支持可重入
答:支持可重入。
在一个同步方法里面调用另一个同步方法,可以正常的执行。
@Slf4j(topic = "s")
public class Demo {
synchronized void test1() throws InterruptedException {
log.debug("test1 start.........");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test2();
log.debug("test1 end.........");
}
synchronized void test2() throws InterruptedException {
log.debug("test2 start.......");
TimeUnit.SECONDS.sleep(1);
log.debug("test2 end.......");
}
public static void main(String[] args) throws InterruptedException {
Demo demo= new Demo();
demo.test1();
}
}
首先调用test1方法获取到了锁,test1方法内部调用了同步方法test2,此时可以正常执行,说明synchronized支持可重入。
控制台的结果如下:
7.synchronized可重入的另一种情况,继承
子类调用同步方法,同步方法内部调用父类的同步方法,可正常执行。
代码如下:
@Slf4j(topic = "s")
public class Demo {
synchronized void test(){
log.debug("demo test start........");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("demo test end........");
}
public static void main(String[] args) {
new Demo2().test();
}
}
@Slf4j(topic = "s")
class Demo2 extends Demo {
@Override
synchronized void test(){
log.debug("demo2 test start........");
super.test();
log.debug("demo2 test end........");
}
}
Demo2是子类,调用了同步方法test,此时锁定的对象就是Demo2实例,内部调用父类的同步方法test(),可正常执行,这是可重入的另一种形式。
控制台输出结果如下:
8.synchronized同步方法内发生异常,是否会释放锁?
这要分两种情况,
- 若对异常进行了处理,则不会释放锁
- 若不处理异常,则会释放锁。
代码如下:
@Slf4j(topic = "s")
public class Demo {
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 {
Demo demo = new Demo();
new Thread(()->{
demo.test();
},"t1").start();
TimeUnit.MILLISECONDS.sleep(1);
new Thread(()->{
demo.test();
}, "t2").start();
}
}
上面的代码对异常进行了处理,此时不会释放锁,t2线程拿不到锁。控制台输出结果如下:
只有t1线程在执行。
若将代码改成如下,不处理异常
@Slf4j(topic = "s")
public class Demo {
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) {
int i = 1 / 0;
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Demo demo = new Demo();
new Thread(() -> {
demo.test();
}, "t1").start();
TimeUnit.MILLISECONDS.sleep(1);
new Thread(() -> {
demo.test();
}, "t2").start();
}
}
此时异常不处理,锁会释放掉,t1线程结束,t2获得到锁执行代码。