Java多线程 - 锁机制

为什么要锁

如果多个线程修改同一个数据对象,那么会对数据造成破坏,出现“意料之外”的结果,显然不是我们想要,所以我们需要线程同步,也就是需要一把“锁”。
举个例子:

package com.johan.syncthread;

public class MainApp {

    public static void main(String[] args) {
        DataClass dataClass = new DataClass(100);
        Thread thread1 = new Thread(new AddDataRunnale(dataClass));
        Thread thread2 = new Thread(new AddDataRunnale(dataClass));
        Thread thread3 = new Thread(new AddDataRunnale(dataClass));
        Thread thread4 = new Thread(new AddDataRunnale(dataClass));
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }

    static class AddDataRunnale implements Runnable {
        private DataClass dataClass;
        public AddDataRunnale(DataClass dataClass) {
            this.dataClass = dataClass;
        }
        @Override
        public void run() {
            for (int i = 1; i < 3; i++) {
                int newData = dataClass.add(i * 10);
                System.out.println("+ " + i * 10 + " = " + newData);
            }
        }
    }

    static class DataClass {
        private int data;
        public DataClass(int data) {
            this.data = data;
            System.out.println("data = " + data);
        }
        public int add(int data) {
            this.data += data;
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return this.data;
        }
        public int getData() {
            return data;
        }
    }

}

这里是4个线程同时操作DataClass对象的data属性,看一下打印结果:

data = 100
+ 10 = 140
+ 10 = 140
+ 10 = 140
+ 10 = 140
+ 20 = 220
+ 20 = 220
+ 20 = 220
+ 20 = 220

这结果显然每次加完的结果是不对的,造成“脏读”现象。我们现在把DataClass的add方法设置为同步方法:

public synchronized int add(int data) {
    this.data += data;
    try {
        Thread.sleep(1);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return this.data;
}

我们再来看看打印结果:

data = 100
+ 10 = 110
+ 10 = 120
+ 20 = 140
+ 10 = 150
+ 10 = 160
+ 20 = 180
+ 20 = 200
+ 20 = 220

最终的结果正确,而且每次加完之后,读取到的也是正确的结果。
通过这个例子,应该认识到多线程同步的重要性了吧。

锁机制

每个Java对象都有一个锁,当有一个线程(A)调用一个实例(如dataClass)的某个非静态synchronized同步方法(例如add)时,此时便获得了这个实例(dataClass)的对象锁。此时,如果有其他线程(B)也想调用该实例(dataClass)的非静态synchronized同步方法(任何一个),因为该实例(dataClass)对象锁已经被前一个线程(A)持有,所以其他线程(B)只能阻塞,进入该对象的锁等待池中,等待获取对象锁。直到前一个线程(A)运行完某个非静态synchronized同步方法(add),就会释放对象锁,其他线程(B)才能开始运行该实例的非静态synchronized同步方法(任何一个)。注意,对于非静态synchronized同步方法,实例(dataClass)便是对象锁。
等同于以下同步代码块:

public int add(int data) {
    // 同步的是this,每个实例this不同,所以每个实例有自己的对象锁
    synchronized (this) {
        this.data += data;
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return this.data;
    }
}

对于静态方法加同步时,原理和非静态不同方法差不多,只是此时的对象锁是整个Class类,在上面的例子中,就是DataClass这个类。
等同于以下同步代码块:

public static int add(int data) {
    synchronized (DataClass.class) {
        this.data += data;
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return this.data;
    }
}
  • 调用同一个对象中非静态同步方法的线程将彼此阻塞。如果是不同对象,则每个线程有自己的对象的锁,线程间彼此互不干预。
  • 调用同一个类中的静态同步方法的线程将彼此阻塞,它们都是锁定在相同的Class对象上。

再说说同步方法和同步代码块的区别:主要是同步的粒度不一样。同步方法,会同步整个方法,粒度比较大。而同步代码块存在于方法里,线程只在运行到同步代码块时才去获取锁,粒度较小。

死锁

来个经典的死锁例子:

package com.johan.syncthread;

public class MainApp {

    public static void main(String[] args) {

        Object lockA = new Object();
        Object lockB = new Object();

        Thread threadA = new Thread(new RunnableA(lockA, lockB));
        Thread threadB = new Thread(new RunnableB(lockA, lockB));

        threadA.start();
        threadB.start();

    }

    public static class RunnableA implements Runnable {
        private Object lockA, lockB;
        public RunnableA(Object lockA, Object lockB) {
            this.lockA = lockA;
            this.lockB = lockB;
        }
        @Override
        public void run() {
            synchronized (lockA) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.err.println("RunnableA get lockA");
                synchronized (lockB) {
                    System.err.println("RunnableA get lockB");
                }
            }
        }
    }

    public static class RunnableB implements Runnable {
        private Object lockA, lockB;
        public RunnableB(Object lockA, Object lockB) {
            this.lockA = lockA;
            this.lockB = lockB;
        }
        @Override
        public void run() {
            synchronized (lockB) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.err.println("RunnableB get lockB");
                synchronized (lockA) {
                    System.err.println("RunnableB get lockA");
                }
            }
        }
    }

}

我们先来看结果:

RunnableA get lockA
RunnableB get lockB

两个线程都只获取了第一个锁,程序便执行不下去了。聪明的你一定发现了猫腻。
我们分析一下,RunnableA获取lockA之后,等待了100毫秒之后,再想获取lockB,无奈此时的lockB已经被RunnableB获取了,所以RunnableA一直再等,但是又不释放lockA。而RunnableB获取到了lockB后,同样等待了100毫秒,想要获取lockA,此时的lockA又被RunnableA持有了,同样只能等。
现在的情况是:
RunnableA持有lockA,等lockB。
RunnableB持有lockB,等lockA。
那么RunnableA和RunnableB两个线程只能耗着,这就造成了“死锁”。

造成死锁的主要原因是,线程间互相等待锁,只要尽量避免多线程同时持有多个锁。即使同时持有多个锁也无所谓,但是不要抱死不放就行,充分利用对象的wait和notify方法。

Java设计的锁

其实Java设计了几种锁供我们使用,其中比较常用的是ReentrantLockReadWriteLock
这里引用博客【Java线程】锁机制:synchronized、Lock、Condition
(1)ReentrantLock

class Outputter1 {    
    private Lock lock = new ReentrantLock();// 锁对象    
    public void output(String name) {           
        lock.lock();      // 得到锁    
        try {    
            for(int i = 0; i < name.length(); i++) {    
                System.out.print(name.charAt(i));    
            }    
        } finally {    
            lock.unlock();// 释放锁    
        }    
    }    
}    

ReentrantLock 类实现了Lock ,它拥有与synchronized 相同的并发性和内存语义,但是添加了类似锁投票、定时锁等候和可中断锁等候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。(换句话说,当许多线程都想访问共享资源时,JVM 可以花更少的时候来调度线程,把更多时间用在执行线程上。)
需要注意的是,用sychronized修饰的方法或者语句块在代码执行完之后锁自动释放,而是用Lock需要我们手动释放锁,所以为了保证锁最终被释放(发生异常情况),要把互斥区放在try内,释放锁放在finally内!!

(2)读写锁ReadWriteLock

class Data {        
    private int data;// 共享数据    
    private ReadWriteLock rwl = new ReentrantReadWriteLock();       
    public void set(int data) {    
        rwl.writeLock().lock();// 取到写锁    
        try {    
            System.out.println(Thread.currentThread().getName() + "准备写入数据");    
            try {    
                Thread.sleep(20);    
            } catch (InterruptedException e) {    
                e.printStackTrace();    
            }    
            this.data = data;    
            System.out.println(Thread.currentThread().getName() + "写入" + this.data);    
        } finally {    
            rwl.writeLock().unlock();// 释放写锁    
        }    
    }       
    public void get() {    
        rwl.readLock().lock();// 取到读锁    
        try {    
            System.out.println(Thread.currentThread().getName() + "准备读取数据");    
            try {    
                Thread.sleep(20);    
            } catch (InterruptedException e) {    
                e.printStackTrace();    
            }    
            System.out.println(Thread.currentThread().getName() + "读取" + this.data);    
        } finally {    
            rwl.readLock().unlock();// 释放读锁    
        }    
    }    
}    
public static void main(String[] args) {    
    final Data data = new Data();      
    //写入  
    for (int i = 0; i < 3; i++) {    
        Thread t = new Thread(new Runnable() {    
            @Override  
    public void run() {    
                for (int j = 0; j < 5; j++) {    
                    data.set(new Random().nextInt(30));    
                }    
            }    
        });  
        t.setName("Thread-W" + i);  
        t.start();  
    }    
    //读取  
    for (int i = 0; i < 3; i++) {    
        Thread t = new Thread(new Runnable() {    
            @Override  
    public void run() {    
                for (int j = 0; j < 5; j++) {    
                    data.get();    
                }    
            }    
        });    
        t.setName("Thread-R" + i);  
        t.start();  
    }    
}    

结果:

Thread-W1准备写入数据  
Thread-W1写入9  
Thread-W1准备写入数据  
Thread-W1写入24  
Thread-W1准备写入数据  
Thread-W1写入12  
Thread-W0准备写入数据  
Thread-W0写入22  
Thread-W0准备写入数据  
Thread-W0写入15  
Thread-W0准备写入数据  
Thread-W0写入6  
Thread-W0准备写入数据  
Thread-W0写入13  
Thread-W0准备写入数据  
Thread-W0写入0  
Thread-W2准备写入数据  
Thread-W2写入23  
Thread-W2准备写入数据  
Thread-W2写入24  
Thread-W2准备写入数据  
Thread-W2写入24  
Thread-W2准备写入数据  
Thread-W2写入17  
Thread-W2准备写入数据  
Thread-W2写入11  
Thread-R2准备读取数据  
Thread-R1准备读取数据  
Thread-R0准备读取数据  
Thread-R0读取11  
Thread-R1读取11  
Thread-R2读取11  
Thread-W1准备写入数据  
Thread-W1写入18  
Thread-W1准备写入数据  
Thread-W1写入1  
Thread-R0准备读取数据  
Thread-R2准备读取数据  
Thread-R1准备读取数据  
Thread-R2读取1  
Thread-R2准备读取数据  
Thread-R1读取1  
Thread-R0读取1  
Thread-R1准备读取数据  
Thread-R0准备读取数据  
Thread-R0读取1  
Thread-R2读取1  
Thread-R2准备读取数据  
Thread-R1读取1  
Thread-R0准备读取数据  
Thread-R1准备读取数据  
Thread-R0读取1  
Thread-R2读取1  
Thread-R1读取1  
Thread-R0准备读取数据  
Thread-R1准备读取数据  
Thread-R2准备读取数据  
Thread-R1读取1  
Thread-R2读取1  
Thread-R0读取1 

与互斥锁定相比,读-写锁定允许对共享数据进行更高级别的并发访问。虽然一次只有一个线程(writer 线程)可以修改共享数据,但在许多情况下,任何数量的线程可以同时读取共享数据(reader 线程)。使用读-写锁定所允许的并发性增强将带来更大的性能提高。

以后会对锁机制有更多的理解,慢慢补回来!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值