在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;

    1. 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;

  1. public Counter() {<br /> count = 0;<br /> }
  2. 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 /> }
  3. //非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 /> }
  4. <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;

  1. 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 /> }
  2. public float getBalance() {<br /> return amount;<br /> }<br />}

/*
账户操作类
*/
class AccountOperator implements Runnable{
private Account account;
public AccountOperator(Account account) {
this.account = account;
}

  1. <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 /> }
  2. public static void main(String[] args) {<br /> Account account = new Account("小明", 10000.0f);<br /> AccountOperator accountOperator = new AccountOperator(account);
  3. 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;

  1. 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 /> }
  2. public float getBalance() {<br /> return amount;<br /> }<br />}

/*
账户操作类
*/
class AccountOperator implements Runnable{
private Account account;
public AccountOperator(Account account) {
this.account = account;
}

  1. @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 /> }
  2. public static void main(String[] args) {<br /> Account account = new Account("小明", 10000.0f);<br /> AccountOperator accountOperator = new AccountOperator(account);
  3. 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;

  1. public ClassThread() {<br /> count = 0;<br /> }
  2. 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 /> }
  3. @Override<br /> public synchronized void run() {<br /> method();<br /> }
  4. 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方法
  • class-name:指定类的Class对象用做锁定对象

    Demo2

    | 1
    2
    3
    4
    5
    6

    | public class BankAccount {
    private Object credential = new Object();

    1. @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 listOfTransactions;
}

| | —- | —- |

我们现在有一个在BankAccount的事务列表。 List对象有许多元素,因此它具有对列表中所有元素的引用。 这里我们使用@GuardedBy(“listOfTransactions”)来指定锁与listOfTransactions所引用的对象相关联。 换句话说,有人必须持有所有事务的锁,以便保持此List对象的锁定。