Contents
  1. 1. 一、基础知识
    1. 1.1. 同步代码块
    2. 1.2. 同步方法
  2. 2. 二、生产者、消费者问题
  3. 3. 三、小结

在看本篇文章之前,我们要写思考以下问题。

是否理解synchronized的含义、明确synchronized关键字修饰普通方法、静态方法和代码块时锁对象的差异。(篇尾会将答案贴出来)

要是还有疑虑的话,这篇文章希望帮你很好的理清。

一、基础知识

我们知道,在多线程共享资源时,由于共享资源的操作不完整,导致共享的数据产生错误。为了保证对共享数据操作的完整性,这种完整性称为共享数据操作的同步(同步就是等待某个线程对某个数据操作完毕后,另一个线程才能开始操作)。在java语言中,用关键字synchronized来声明一个操作共享数据的一段代码块或一个方法。可以实现“对象互斥锁”。

根据关键字synchronized修饰的对象不同,有同步代码块和同步方法两种。

同步代码块

synchronized(<同步对象名>){需要同步的代码}

同步方法

synchronized <方法返回值><方法名>(参数列表){方法体}
在这里要穿插一个知识点,java的乐观锁与悲观锁

悲观锁(Pessimistic Lock),顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。

悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。

Java synchronized 就属于悲观锁的一种实现,每次线程要修改数据时都先获得锁,保证同一时刻只有一个线程能操作数据,其他线程则会被block,当一个线程调用同一个同步方法时,它首先判断该方法上的锁是否已被锁定,如果未锁,则执行该方法,同时给这个方法上锁,已独占方法运行方法体,运行完后给这个方法释放锁。如果方法被锁定,则线程必须等待,直至方法锁被占用线程释放,

乐观锁(Optimistic Lock),顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在提交更新的时候会判断一下在此期间别人有没有去更新这个数据。乐观锁适用于读多写少的应用场景,这样可以提高吞吐量。

二、生产者、消费者问题

“生产者”不断生产产品并将其放在共享的产品队列中,而“消费者”则不断的从产品队列中取出ch

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
public class ProducerConsumerDemo1 {
class ShareData{ //共享数据
private char c;
public synchronized void putShareChar(char c){//生产,同步方法,一个放
this.c=c;
}
public synchronized char getShareChar(){//消费,同步方法,一个取
return this.c;
}
}
//生产者线程
class Producer implements Runnable{
private ShareData s;
Producer(ShareData s){
this.s=s;
}
@Override
public void run() {
for(char ch='A';ch<='H';ch++){
s.putShareChar(ch);
System.out.println("生产者生产"+ch+"产品");
try {
Thread.sleep((int)Math.random()*100);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
//消费者线程
class Consumer implements Runnable{
private ShareData s;
Consumer(ShareData s){
this.s=s;
}
@Override
public void run() {
char ch;
do{
ch=s.getShareChar();
System.out.println("消费者消费"+ch+"产品");
try {
Thread.sleep((int)Math.random()*100);
}catch (InterruptedException e){
e.printStackTrace();
}
}while (ch!='H');
}
}
public static void main(String args[]){
ShareData s=new ShareData();
Producer producer=new Producer(s);//生产者线程
Consumer consumer=new Consumer(s);//消费者线程
Thread p=new Thread(producer);
Thread c=new Thread(consumer);
p.setPriority(Thread.MAX_PRIORITY);
c.setPriority(Thread.MAX_PRIORITY);
p.start();
c.start();
}
}

程序运行结果:

消费者会取到相同数据。这种虽然使用了synchronized,但还是不同步,所以还会引入wait()/notify()方法,消费者在没有物品消费的时候,就用wait()在条件队列等待,当生产者生产出了一个物品后,就用notify()来将消费者唤醒。通过这种方式避免重复生产和重复消费的情况,这里就不用详细的代码去实现了。

三、小结

Java多线程中的同步机制会对资源进行加锁,保证在同一时间只有一个线程可以操作对应资源,避免多程同时访问相同资源发生冲突。Synchronized是Java中的关键字,它是一种同步锁,可以实现同步机制。

Synchronized主修修饰对象为以下三种:

  1. 修饰普通方法 一个对象中的加锁方法只允许一个线程访问。但要注意这种情况下锁的是访问该方法的实例对象, 如果多个线程不同对象访问该方法,则无法保证同步。(锁的是这个对象的锁)

  2. 修饰静态方法 由于静态方法是类方法, 所以这种情况下锁的是包含这个方法的类,也就是类对象;这样如果多个线程不同对象访问该静态方法,也是可以保证同步的。(锁的是这个类对象的锁)

  3. 修饰代码块 其中普通代码块 如Synchronized(obj) 这里的obj 可以为类中的一个属性、也可以是当前的对象,它的同步效果和修饰普通方法一样;Synchronized方法 (obj.class)静态代码块它的同步效果和修饰静态方法类似。

  4. 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class A {
    public synchronized void foo(){

    }
    public static synchronized void zoo(){

    }
    public void boo(){
    A a=new A();
    synchronized (a){
    }//锁的是对象a的锁
    synchronized (A.class){
    }//锁的是对象A.class的锁
    }
    }
1
2
3
A a=new A();
a.foo();//锁的是对象a的锁
a.zoo();//锁的是对象A.class的锁

1.当修饰的是普通方法时,遵循一个对象一个锁的规律,如果是同一个对象调用会互斥,所有的synchronized修饰的方法都会同步,如果创建了两个对象,则互不影响。

2.修饰类时,或者修饰静态方法时,即便创建了两个对象,同一个类仍会被锁住,类中的不同对象获取的时同一把锁。