在Java中,我们一般会使用synchronized给一段在高并发情况下会产生异常的代码加上一个同步锁,实现在运行该代码的时候,多个线程会去先竞争这个同步锁,获得这同步锁的进程会去执行该段进程,其他线程处于wait状态,直到获得该锁的线程运行完毕释放锁后,其他线程会再次进入竞争状态,竞争该锁的使用权…以此类推!
- 线程1、线程2、线程3处于就绪状态
- 三个线程竞争synchronized同步锁
- 线程1夺到锁的使用权,线程2、3恢复到就绪状态
- 线程1拿着同步锁进行读写操作,期间没有任何线程打扰
- 线程1执行完成后,将同步锁归还(释放)
- 处于就绪状态的线程2、3进行竞争锁的使用权】
-
没有synchronized的后果
若是没有synchronized同步锁,举个简单的例子,一段代码执行需要一定的时间,若是一个线程正在对一个共享数据进行操作中,另一个线程也去访问该共享数据,这时候第一个正在执行的线程没有返回他的执行结果,也因此第二个线程读取的共享数据是源数据,也就是没有更改之前的,这就是所谓的“脏数据”。
Example:
小明将自己的零花钱存在一个银行卡里面(1000元),小明的爸爸不放心,则会不定时的去查小明的账户,小明这天刚拿着爸爸给的100元零花钱去存,小明的老爸也想看看小明把自己刚给他的100元是不是如他所说的存了起来,两个人去了两家不同的自动取款机,当小明把钱放进去正在点钞,还没有入账的时候,小明的父亲输入完密码,数据读取中,因为点钞需要一定的时间,在进入账户之前,小明的爸爸已经查到了只有1000元,气呼呼地准备回家去揍小明,小明这时候刚点完钞,入了账!!!请问:小明这次的被打冤枉不冤枉??
放在进程中i就是一个线程进行i++操作,读取–》+1–》写入,而另一个需要使用i的数据,而他在读取的时候,i++正在进行+1操作!!!这时候我们读取的i会丢失1.这种不安全的线程操作!!
我们应该将该方法实现原子性,是一个原子方法,我们加上synchronized同步锁,就不会出现多个线程同时访问一个共享数据的情况,也就不会出现所谓的“脏数据”了!使用方法
修饰一个代码块
一个线程访问一个对象中的synchronized(this)同步代码块时,其他试图访问该对象的线程将被阻塞。我们看下面一个例子:
Deom1
| 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24| public class implements {
private static int count;public SyncThread() {<br /> count = 0;<br /> }
public void run() {
synchronized(this) {
for (int i = 0; i < 5; i++) {
try {
System.out.println(Thread.currentThread().getName() + “:” + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}public int getCount() {
return count;
}
}| | —- | —- |
Syncthread的调用
| 1
2
3
4
5
| SyncThread syncThread = new SyncThread();
Thread thread1 = new Thread(syncThread, “SyncThread1”);
Thread thread2 = new Thread(syncThread, “SyncThread2”);
thread1.start();
thread2.start();
| | —- | —- |
Result:
| 1
2
3
4
5
6
7
8
9
10
| SyncThread1:0
SyncThread1:1
SyncThread1:2
SyncThread1:3
SyncThread1:4
SyncThread2:5
SyncThread2:6
SyncThread2:7
SyncThread2:8
SyncThread2:9
| | —- | —- |
该代码的运行结果中未出现交互出现Thread1、2的运行结果,也就是两个线程对同一个对象进行操作,然后1不放锁,2运行不了!!!
我们试一下改一下调用方式:
| 1
2
3
4
| Thread thread1 = new Thread(new SyncThread(), “SyncThread1”);
Thread thread2 = new Thread(new SyncThread(), “SyncThread2”);
thread1.start();
thread2.start();
| | —- | —- |
我们就会发现结果截然不同了呢!!!
result:
| 1
2
3
4
5
6
7
8
9
10
| SyncThread1:0
SyncThread2:1
SyncThread1:2
SyncThread2:3
SyncThread1:4
SyncThread2:5
SyncThread2:6
SyncThread1:7
SyncThread1:8
SyncThread2:9
| | —- | —- |
发现了什么!!交替执行了!!因为这里的synchronized是给这个SynThread产生的对象加了同步锁,而我们两个线程使用在两个不同的线程上!!!所以这里不会存在竞态关系,故只需要线程抢到CPU资源就可以运行了,根据时间片的切分,会不断的切换线程进行执行!!就出现了上面的结果。
Demo2
我们尝试在一个方法中,加入一个含有锁的代码块以及一个不含有锁的代码块,并且该类还有另一个方法(未加锁),被统一对象调用我们看一看结果
| 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
*/
public class Counter implements Runnable{
private int count;
public Counter() {<br /> count = 0;<br /> }
public void countAdd() {<br /> synchronized(this) {<br /> for (int i = 0; i < 5; i ++) {<br /> try {<br /> System.out.println("有锁的代码块"+Thread.currentThread().getName() + ":" + (count++));<br /> Thread.sleep(100);<br /> } catch (InterruptedException e) {<br /> e.printStackTrace();<br /> }<br /> }<br /> }<br /> //同一个方法中的未加锁代码块<br /> for (int j = 0; j < 5; j ++) {<br /> try {<br /> System.out.println("没有锁的代码块"+Thread.currentThread().getName() + ":" + (count++));<br /> Thread.sleep(100);<br /> } catch (InterruptedException e) {<br /> e.printStackTrace();<br /> }<br /> }<br /> }
//非synchronized代码块,未对count进行读写操作,所以可以不用synchronized<br /> public void printCount() {<br /> for (int i = 0; i < 5; i ++) {<br /> try {<br /> System.out.println(Thread.currentThread().getName() + " count:" + count);<br /> Thread.sleep(100);<br /> } catch (InterruptedException e) {<br /> e.printStackTrace();<br /> }<br /> }<br /> }
<br /> public void run() {<br /> String threadName = Thread.currentThread().getName();<br /> if (threadName.equals("A")) {<br /> countAdd();<br /> } else if (threadName.equals("B")) {<br /> printCount();<br /> }<br /> }<br />}
| | —- | —- |
调用代码:
| 1
2
3
4
5
6
7
| public static void main(String[] args) {
Counter counter = new Counter();
Thread thread1 = new Thread(counter, “A”);
Thread thread2 = new Thread(counter, “B”);
thread1.start();
thread2.start();
}
| | —- | —- |
执行结果:
| 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| B count:0
有锁的代码块A:0
有锁的代码块A:1
B count:2
B count:2
有锁的代码块A:2
B count:3
有锁的代码块A:3
B count:4
有锁的代码块A:4
没有锁的代码块A:5
没有锁的代码块A:6
没有锁的代码块A:7
没有锁的代码块A:8
没有锁的代码块A:9
| | —- | —- |
这里我们就可以很清楚的看到以下现象:
- 同一个方法中,一个代码块加synchronized同步锁,另一个不加,按顺序执行。(当前执行对象的锁被获取了)
- 不同的方法,一个加锁,另一个未加,则互不影响!
给某个对象加锁
Demo3
上面小明的案例中,我们为了不让小明挨打,我们可以对该账户加锁,只要有用户正在操作该用户,那么我们就可以让其他人不能操作!
演示代码:(未加锁)
| 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
| public class Account {
private String name;
private float amount;
public Account(String name, float amount) {<br /> this.name = name;<br /> this.amount = amount;<br /> }<br /> //存钱<br /> public void deposit(float amt) {<br /> amount += amt;<br /> try {<br /> Thread.sleep(100);<br /> } catch (InterruptedException e) {<br /> e.printStackTrace();<br /> }<br /> }<br /> //取钱<br /> public void withdraw(float amt) {<br /> amount -= amt;<br /> try {<br /> Thread.sleep(100);<br /> } catch (InterruptedException e) {<br /> e.printStackTrace();<br /> }<br /> }
public float getBalance() {<br /> return amount;<br /> }<br />}
/*
账户操作类
*/
class AccountOperator implements Runnable{
private Account account;
public AccountOperator(Account account) {
this.account = account;
}
<br /> public void run() {<br /> //synchronized (account) {<br /> account.deposit(500);<br /> account.withdraw(500);<br /> System.out.println(Thread.currentThread().getName() + ":" + account.getBalance());<br /> //}<br /> }
public static void main(String[] args) {<br /> Account account = new Account("小明", 10000.0f);<br /> AccountOperator accountOperator = new AccountOperator(account);
final int THREAD_NUM = 5;<br /> Thread threads[] = new Thread[THREAD_NUM];<br /> for (int i = 0; i < THREAD_NUM; i ++) {<br /> threads[i] = new Thread(accountOperator, "Thread" + i);<br /> threads[i].start();<br /> }<br /> }<br />}
| | —- | —- |
运行结果:
| 1
2
3
4
5
| Thread4:10500.0
Thread0:10500.0
Thread3:10500.0
Thread2:10500.0
Thread1:10500.0
| | —- | —- |
这里出现了异常!我每次在同一个方法中存500,取500,输出应该是10000,而不是10500,很有可能的是,在存钱命令执行完毕之后,输出语句先分配到了内存进行了执行,之后才执行了取钱操作
我们加上锁看看:
| 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
| package concurrency;
/*
@author :DengSiYuan
@date :2019/3/15 8:59
@desc :
*/
public class Account {
private String name;
private float amount;
public Account(String name, float amount) {<br /> this.name = name;<br /> this.amount = amount;<br /> }<br /> //存钱<br /> public void deposit(float amt) {<br /> amount += amt;<br /> try {<br /> Thread.sleep(100);<br /> } catch (InterruptedException e) {<br /> e.printStackTrace();<br /> }<br /> }<br /> //取钱<br /> public void withdraw(float amt) {<br /> amount -= amt;<br /> try {<br /> Thread.sleep(100);<br /> } catch (InterruptedException e) {<br /> e.printStackTrace();<br /> }<br /> }
public float getBalance() {<br /> return amount;<br /> }<br />}
/*
账户操作类
*/
class AccountOperator implements Runnable{
private Account account;
public AccountOperator(Account account) {
this.account = account;
}
@Override<br /> public void run() {<br /> synchronized (account) {<br /> account.deposit(500);<br /> account.withdraw(500);<br /> System.out.println(Thread.currentThread().getName() + ":" + account.getBalance());<br /> }<br /> }
public static void main(String[] args) {<br /> Account account = new Account("小明", 10000.0f);<br /> AccountOperator accountOperator = new AccountOperator(account);
final int THREAD_NUM = 5;<br /> Thread threads[] = new Thread[THREAD_NUM];<br /> for (int i = 0; i < THREAD_NUM; i ++) {<br /> threads[i] = new Thread(accountOperator, "Thread" + i);<br /> threads[i].start();<br /> }<br /> }<br />}
| | —- | —- |
演示结果:
| 1
2
3
4
5
| Thread0:10000.0
Thread4:10000.0
Thread3:10000.0
Thread2:10000.0
Thread1:10000.0
| | —- | —- |
这里我们对帐户进行了加锁处理,我们就会发现这里的结果是正确的,在存钱的线程进行完毕释放锁之后,取钱的线程才会获得锁的使用权,进行取钱操作!!
当我们没有特别的加锁对象的时候,只是为了让一段代码实现同步,为了减少开销,我们会使用private byte[] lock = new byte[0];作为锁进行处理
修饰一个方法
Synchronized修饰一个方法很简单,就是在方法的前面加synchronized,public synchronized void method(){}; synchronized修饰方法和修饰一个代码块类似,只是作用范围不一样,修饰代码块是大括号括起来的范围,而修饰方法范围是整个函数。如将【Demo1】中的run方法改成如下的方式,实现的效果一样。
Demo4
| 1
2
3
4
5
6
7
8
9
10
| public synchronized void run() {
for (int i = 0; i < 5; i ++) {
try {
System.out.println(Thread.currentThread().getName() + “:” + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
| | —- | —- |
修饰方法的两种写法
写法一: 修饰在方法返回参数之前
| 1
2
3
| public synchronized void method(){
}
| | —- | —- |
写法二: 修饰在方法内,将方法中的所有代码包含起来
| 1
2
3
4
5
| public void method(){
synchronized(this) {
}
}
| | —- | —- |
修饰方法时需要注意的
- synchronized关键字不能继承
虽然可以使用synchronized来定义方法,但synchronized并不属于方法定义的一部分,因此,synchronized关键字不能被继承。如果在父类中的某个方法使用了synchronized关键字,而在子类中覆盖了这个方法,在子类中的这个方法默认情况下并不是同步的,而必须显式地在子类的这个方法中加上synchronized关键字才可以。当然,还可以在子类方法中调用父类中相应的方法,这样虽然子类中的方法不是同步的,但子类调用了父类的同步方法,因此,子类的方法也就相当于同步了。这两种方式的例子代码如下:
| 1
2
3
4
5
6
| class Parent {
public synchronized void method() { }
}
class Child extends Parent {
public synchronized void method() { }
}
| | —- | —- |
在子类方法中调用父类的同步方法
| 1
2
3
4
5
6
| class Parent {
public synchronized void method() { }
}
class Child extends Parent {
public void method() { super.method(); }
}
| | —- | —- |
- 在定义接口方法时不能使用synchronized关键字
构造方法不能使用synchronized关键字,但可以使用synchronized代码块来进行同步。
修饰一个静态方法
我们知道静态方法是属于类的而不属于对象的。同样的,synchronized修饰的静态方法锁定的是这个类的所有对象。
Demo5
| 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37| /*
@author :DengSiYuan
@date :2019/3/15 9:42
@desc :
*/
public class SyncThread implements Runnable {
private static int count;public SyncThread() {
count = 0;
}public synchronized static void method() {
for (int i = 0; i < 5; i ++) {
try {
System.out.println(Thread.currentThread().getName() + “:” + (count++));
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}@Override
public synchronized void run() {
method();
}public static void main(String[] args) {
SyncThread syncThread1 = new SyncThread();
SyncThread syncThread2 = new SyncThread();
Thread thread1 = new Thread(syncThread1, “SyncThread1”);
Thread thread2 = new Thread(syncThread2, “SyncThread2”);
thread1.start();
thread2.start();
}
}| | —- | —- |
执行结果:
| 1
2
3
4
5
6
7
8
9
10
| SyncThread1:0
SyncThread1:1
SyncThread1:2
SyncThread1:3
SyncThread1:4
SyncThread2:5
SyncThread2:6
SyncThread2:7
SyncThread2:8
SyncThread2:9
| | —- | —- |
syncThread1和syncThread2是SyncThread的两个对象,但在thread1和thread2并发执行时却保持了线程同步。这是因为run中调用了静态方法method,而静态方法是属于类的,所以syncThread1和syncThread2相当于用了同一把锁。这与Demo1是不同的。
修饰一个类
| 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| public class ClassThread implements Runnable {
private static int count;
public ClassThread() {<br /> count = 0;<br /> }
public static void method() {<br /> synchronized(SyncThread.class) {<br /> for (int i = 0; i < 5; i ++) {<br /> try {<br /> System.out.println(Thread.currentThread().getName() + ":" + (count++));<br /> Thread.sleep(100);<br /> } catch (InterruptedException e) {<br /> e.printStackTrace();<br /> }<br /> }<br /> }<br /> }
@Override<br /> public synchronized void run() {<br /> method();<br /> }
public static void main(String[] args) {<br /> ClassThread classThread1 = new ClassThread();<br /> ClassThread classThread2 = new ClassThread();<br /> Thread thread1 = new Thread(classThread1, "classThread1");<br /> Thread thread2 = new Thread(classThread2, "classThread2");<br /> thread1.start();<br /> thread2.start();<br /> }<br />}
| | —- | —- |
运行结果:
| 1
2
3
4
5
6
7
8
9
10
| classThread1:0
classThread1:1
classThread1:2
classThread1:3
classThread1:4
classThread2:5
classThread2:6
classThread2:7
classThread2:8
classThread2:9
| | —- | —- |
其效果和【Demo5】是一样的,synchronized作用于一个类T时,是给这个类T加锁,T的所有对象用的是同一把锁。
总结
- A. 无论synchronized关键字加在方法上还是对象上,如果它作用的对象是非静态的,则它取得的锁是对象;如果synchronized作用的对象是一个静态方法或一个类,则它取得的锁是对类,该类所有的对象同一把锁。
- B. 每个对象只有一个锁(lock)与之相关联,谁拿到这个锁谁就可以运行它所控制的那段代码。
C. 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
@GuardedBy
了解到前面的sychronized之后,偶然间看到了GuardedBy这个注解,它可以去替代sychronized关键字去加锁。
Demo1
| 1
2
3
4| public class foo {
@GuardedBy(“this”)
public String str;
}| | —- | —- |
该注解对String str进行了加锁,我们下面介绍以下该注解的参数都有哪些
参数含义
- this:在其类中定义字段的对象的固有锁
- class-name.this:对于内部类,可能有必要消除“this”的歧义;class-name.this指定允许您指定“this”引用的意图
- itself: 仅供参考字段; 字段引用的对象
- field-name:锁对象由字段名指定的(实例或静态)字段引用。
- class-name.field-name:锁对象由class-name.field-name指定的静态字段引用
- method-name():锁对象通过调用nil-ary方法
-
Demo2
| 1
2
3
4
5
6| public class BankAccount {
private Object credential = new Object();@GuardedBy("credential")<br /> private int amount;<br />}
| | —- | —- |
在上面的代码片段中,当有人获得了凭据的同步锁定时,可以访问金额,因此,BankAccount中的金额由凭据保护。 让我们给这个类添加一些东西.
| 1
2
3
4
5
6
7
8
9
| public class BankAccount {
private Object credential = new Object();
@GuardedBy(“credential”)
private int amount;
@GuardedBy(“listOfTransactions”)
private List
}
| | —- | —- |
我们现在有一个在BankAccount的事务列表。 List对象有许多元素,因此它具有对列表中所有元素的引用。 这里我们使用@GuardedBy(“listOfTransactions”)来指定锁与listOfTransactions所引用的对象相关联。 换句话说,有人必须持有所有事务的锁,以便保持此List对象的锁定。