线程同步与锁

时光怂恿深爱的人放手 提交于 2020-02-11 14:35:41

1、线程同步问题(synchronized)

Brian Goetz给出了下述“同步格言”:“如果向一个变量写入值,而这个变量接下来可能会被另一个线程读取,或者,从一个变量读值,而这个变量可能是之前被另一个线程写入的,此时必须使用同步。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GGmGRS4T-1581397703945)(images/12.png)]

在大多数实际的多线程应用中,两个或两个以上的线程需要共享对同一数据的存取

如果两个线程存取相同的对象,并且每一个线程都调用了一个修改该对象状态的方法,将会发生什么呢?

可以想象,线程彼此踩了对方的脚。根据各线程访问数据的次序,可能会产生讹误的对象。

为了避免多线程引起的对共享数据的讹误,必须学习如何同步存取。

真正的问题在于方法在执行的过程中可能被中断

如果能够确保线程在失去控制之前方法运行完成,一般不会出错

1.1、Web123606

package CoreJavaColume.Chapter14;

/**
 * @Author Zhou  jian
 * @Date 2020 ${month}  2020/2/8 0008  14:34
 *
 * 线程不安全:数据有负数和相同数据的情况
 */
public class UnsafeTest01 {

    public static void main(String[] args) {

        UnsafeWeb12306 web = new UnsafeWeb12306();

        //多个代理
        new Thread(web,"吗").start();
        new Thread(web,"吗农").start();
        new Thread(web,"吗皇").start();

    }


}

 class UnsafeWeb12306  implements  Runnable{

    //票数
    private int ticketNums = 10;
    private boolean flag = true;

    @Override
    public void run() {
        while (flag) {
            test();
        }
    }

    public void test(){
            if(ticketNums<0) flag=false;
            //模拟延时
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"---->"+ticketNums--);
        }






    public static void main(String[] args) {
        //一份资源;多份代理
        Web12306 web = new Web12306();

        //多个代理
        new Thread(web,"码农").start();
        new Thread(web,"码出").start();
        new Thread(web,"码黄").start();

    }



}


1.2、银行账户取钱

package CoreJavaColume.Chapter14;

/**
 * @Author Zhou  jian
 * @Date 2020 ${month}  2020/2/8 0008  14:42
 * 线程不安全:取钱
 */
public class UnsafeTest02 {

    public static void main(String[] args) {

        //账户
        Account account = new Account(100,"结婚礼金");

        Drawing you = new Drawing(account,80,"可悲的你");
        Drawing wife = new Drawing(account,90,"happy的它");

        you.start();//启动线程  开始取钱
        wife.start();
    }



}




class Account{
    int money;
    String name;

    public Account(int money, String name) {
        this.money = money;
        this.name = name;
    }
}

//模拟取款
class Drawing extends  Thread{

    Account account;//取钱的账户
    int drawingMoney;//取的钱数
    int packetTotal;//取的总数


    public Drawing(Account account, int drawingMoney,String name) {
        super(name);
        this.account = account;
        this.drawingMoney = drawingMoney;
    }

    @Override
    public void run() {

        //账户余额不够
        if(account.money-drawingMoney<0) return;

        //模拟网络延时
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        account.money-=drawingMoney;
        packetTotal +=drawingMoney;
        System.out.println(this.getName()+"---->账户余额为:"+account.money);
        System.out.println(this.getName()+"---->口袋的前为:"+packetTotal);
    }
}

1.3、线程容器

package CoreJavaColume.Chapter14;

import java.util.ArrayList;
import java.util.List;

/**
 * @Author Zhou  jian
 * @Date 2020 ${month}  2020/2/8 0008  14:58
 * 测试容器
 */
public class UnsafeTest03 {

    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        //开启10000个线程
        for(int i=0;i<10000;i++){
            new Thread(()->{list.add(Thread.currentThread().getName());}).start();
        }
        System.out.println(list.size());
    }    
    
    
}

并发三要素

  • 同一个对象
  • 多个线程
  • 同时访问

如何保证线程安全

2、可重入锁( ReentrantLock )

2.0、介绍

  • [Lock(https://docs.oracle.com/en/java/javase/13/docs/api/java.base/java/util/concurrent/locks/Lock.html)具有与使用synchronized方法和语句访问的隐式监视器锁相同的基本行为和语义 的可重入互斥,但具有扩展的功能。
  • A ReentrantLock是最后成功锁定但尚未解锁的线程所拥有的。
  • 锁是可重入的,因为线程可以重复的获得已持有的锁。锁保持一个持有计数器(hold count)用来跟踪lock方法的嵌套调用。线程在每一次调用lock都要调用unlock来释放锁。由于这一特性,被一个锁保护的代码可以调用另一个使用相同的锁的方法。
  • ReentrantLock可以实现两种锁:公平锁与非公平锁公平锁是指当锁可用时,在锁上等待时间最长的线程将获得锁的使用权;而非公平锁则随机分配这种使用权。和synchronized一样,默认的ReentrantLock实现是非公平锁,因为相比公屏锁,非公平锁性能更好。当然公平锁能防止饥饿,某些情况下也很有用
public class ReentrantLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = 7373984872572414699L;
    /** Synchronizer providing all implementation mechanics */
    //这个用来实现:公平锁与非公平锁
    private final Sync sync;
    
    abstract void lock();
    
      /**
     * Creates an instance of {@code ReentrantLock} with the
     * given fairness policy.
     *
     * @param fair {@code true} if this lock should use a fair ordering policy
     		创建公平锁与非公平锁
     */
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }
    
      /**
     * Acquires the lock.  获取锁
     *
     * <p>Acquires the lock if it is not held by another thread and returns
     * immediately, setting the lock hold count to one.
     *
     * <p>If the current thread already holds the lock then the hold
     * count is incremented by one and the method returns immediately.
     *
     * <p>If the lock is held by another thread then the
     * current thread becomes disabled for thread scheduling
     * purposes and lies dormant until the lock has been acquired,
     * at which time the lock hold count is set to one.
     */
    public void lock() {
        sync.lock();
    }
    
    
      /**
     * Attempts to release this lock.
     *
     * <p>If the current thread is the holder of this lock then the hold
     * count is decremented.  If the hold count is now zero then the lock
     * is released.  If the current thread is not the holder of this
     * lock then {@link IllegalMonitorStateException} is thrown.
     *
     * @throws IllegalMonitorStateException if the current thread does not
     *         hold this lock
     */
    public void unlock() {
        sync.release(1);
    }
    
     /**
     * Returns a {@link Condition} instance for use with this
     * {@link Lock} instance.
     *
     * <p>The returned {@link Condition} instance supports the same
     * usages as do the {@link Object} monitor methods ({@link
     * Object#wait() wait}, {@link Object#notify notify}, and {@link
     * Object#notifyAll notifyAll}) when used with the built-in
     * monitor lock.
     *
     * <ul>
     *
     * <li>If this lock is not held when any of the {@link Condition}
     * {@linkplain Condition#await() waiting} or {@linkplain
     * Condition#signal signalling} methods are called, then an {@link
     * IllegalMonitorStateException} is thrown.
     *
     * <li>When the condition {@linkplain Condition#await() waiting}
     * methods are called the lock is released and, before they
     * return, the lock is reacquired and the lock hold count restored
     * to what it was when the method was called.
     *
     * <li>If a thread is {@linkplain Thread#interrupt interrupted}
     * while waiting then the wait will terminate, an {@link
     * InterruptedException} will be thrown, and the thread's
     * interrupted status will be cleared.
     *
     * <li> Waiting threads are signalled in FIFO order.
     *
     * <li>The ordering of lock reacquisition for threads returning
     * from waiting methods is the same as for threads initially
     * acquiring the lock, which is in the default case not specified,
     * but for <em>fair</em> locks favors those threads that have been
     * waiting the longest.
     *
     * </ul>
     *
     * @return the Condition object
     */
    public Condition newCondition() {
        return sync.newCondition();
    }
    

2.1、使用ReentranLock

ReentrantLock保护代码块的基本结构如下

myLock.lock();//a ReentrantLock object

try{
    critical section
}
finally
{
    myLock.unlock();
}

这一结构确保任何时刻只有一个线程进入临界区,

一旦一个线程封锁了锁对象,其他任何线程都无法通过lock语句。

当其他线程调用lock时,它们被阻塞,直到第一个线程释放锁对象

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-leyl6VhM-1581397703947)(images/31.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W4awvsR9-1581397703948)(images/29.png)]

2.2、ReentrantLock可响应中断

当使用synchronized实现锁时,阻塞在锁上的线程除非获得锁否则将一直等待下去,也就是说这种无限等待获取锁的行为无法被中断。ReentrantLock给我们提供了一个可以响应中断的获取锁的方法lockInterruptibly()。该方法可以用来解决死锁问题。

public class ReentrantLockTest {
    static Lock lock1 = new ReentrantLock();//创建可重入锁1
    static Lock lock2 = new ReentrantLock();//创建可重入锁2
    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(new ThreadDemo(lock1, lock2));//该线程先获取锁1,再获取锁2
        Thread thread1 = new Thread(new ThreadDemo(lock2, lock1));//该线程先获取锁2,再获取锁1
        
        thread.start();//开启线程
        thread1.start();
        
        thread.interrupt();//是第一个线程中断
    }

    static class ThreadDemo implements Runnable {
        Lock firstLock;
        Lock secondLock;
        public ThreadDemo(Lock firstLock, Lock secondLock) {
            this.firstLock = firstLock;
            this.secondLock = secondLock;
        }
        @Override
        public void run() {
            try {
                firstLock.lockInterruptibly();
                TimeUnit.MILLISECONDS.sleep(10);//更好的触发死锁
                secondLock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                firstLock.unlock();
                secondLock.unlock();
                System.out.println(Thread.currentThread().getName()+"正常结束!");
            }
        }
    }
}

构造死锁场景:创建两个子线程,子线程在运行时会分别尝试获取两把锁。其中一个线程先获取锁1在获取锁2,另一个线程正好相反。如果没有外界中断,该程序将处于死锁状态永远无法停止。我们通过使其中一个线程中断,来结束线程间毫无意义的等待。被中断的线程将抛出异常,而另一个线程将能获取锁后正常结束。

2.3、获取锁限时等待(更好解决死锁问题)

ReentrantLock还给我们提供了获取锁限时等待的方法tryLock(),可以选择传入时间参数,表示等待指定的时间,无参则表示立即返回锁申请的结果:true表示获取锁成功,false表示获取锁失败。我们可以使用该方法配合失败重试机制来更好的解决死锁问题。

更好的解决死锁的例子

public class ReentrantLockTest {
    static Lock lock1 = new ReentrantLock();
    static Lock lock2 = new ReentrantLock();
    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(new ThreadDemo(lock1, lock2));//该线程先获取锁1,再获取锁2
        Thread thread1 = new Thread(new ThreadDemo(lock2, lock1));//该线程先获取锁2,再获取锁1
        thread.start();
        thread1.start();
    }

    static class ThreadDemo implements Runnable {
        Lock firstLock;
        Lock secondLock;
        public ThreadDemo(Lock firstLock, Lock secondLock) {
            this.firstLock = firstLock;
            this.secondLock = secondLock;
        }
        @Override
        public void run() {
            try {
                while(!lock1.tryLock()){
                    TimeUnit.MILLISECONDS.sleep(10);
                }
                while(!lock2.tryLock()){
                    lock1.unlock();
                    TimeUnit.MILLISECONDS.sleep(10);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                firstLock.unlock();
                secondLock.unlock();
                System.out.println(Thread.currentThread().getName()+"正常结束!");
            }
        }
    }
}

线程通过调用tryLock()方法获取锁,第一次获取锁失败时会休眠10毫秒,然后重新获取,直到获取成功。第二次获取失败时,首先会释放第一把锁,再休眠10毫秒,然后重试直到成功为止程获取第二把锁失败时将会释放第一把锁,这是解决死锁问题的关键,避免了两个线程分别持有一把锁然后相互请求另一把锁

2.4、结合Condition实现等待通知机制(线程之间的通信)

使用synchronized结合Object上的waitnotify方法可以实现线程间的等待通知机制。ReentrantLock结合Condition接口同样可以实现这个功能。而且相比前者使用起来更清晰也更简单。

2.4.1、Condition使用简介

Condition由ReentrantLock对象创建,并且可以同时创建多个

  • 一个锁对象可以有一个或多个相关的条件对象
  • 习惯上给每一个条件对象命名为可以反映它所表达的条件的名字
 /**
     * Returns a {@link Condition} instance for use with this
     * {@link Lock} instance.
     *
     * <p>The returned {@link Condition} instance supports the same
     * usages as do the {@link Object} monitor methods ({@link
     * Object#wait() wait}, {@link Object#notify notify}, and {@link
     * Object#notifyAll notifyAll}) when used with the built-in
     * monitor lock.
     *
     * <ul>
     *
     * <li>If this lock is not held when any of the {@link Condition}
     * {@linkplain Condition#await() waiting} or {@linkplain
     * Condition#signal signalling} methods are called, then an {@link
     * IllegalMonitorStateException} is thrown.
     *
     * <li>When the condition {@linkplain Condition#await() waiting}
     * methods are called the lock is released and, before they
     * return, the lock is reacquired and the lock hold count restored
     * to what it was when the method was called.
     *
     * <li>If a thread is {@linkplain Thread#interrupt interrupted}
     * while waiting then the wait will terminate, an {@link
     * InterruptedException} will be thrown, and the thread's
     * interrupted status will be cleared.
     *
     * <li> Waiting threads are signalled in FIFO order.
     *
     * <li>The ordering of lock reacquisition for threads returning
     * from waiting methods is the same as for threads initially
     * acquiring the lock, which is in the default case not specified,
     * but for <em>fair</em> locks favors those threads that have been
     * waiting the longest.
     *
     * </ul>
     *
     * @return the Condition object
     */
    public Condition newCondition() {
        return sync.newCondition();
    }

Condition接口在使用前必须先调用ReentrantLocklock()方法获得锁。

之后调用Condition接口的await()将释放锁,并且在该Condition上等待,直到有其他线程调用Conditionsignal()方法唤醒线程。使用方式和wait,notify类似。

例子

public class ConditionTest {

    static ReentrantLock lock = new ReentrantLock();
    static Condition condition = lock.newCondition();//获取Codition
    public static void main(String[] args) throws InterruptedException {

        lock.lock();
        new Thread(new SignalThread()).start();
        System.out.println("主线程等待通知");
        try {
            condition.await();
        } finally {
            lock.unlock();
        }
        System.out.println("主线程恢复运行");
    }
    static class SignalThread implements Runnable {

        @Override
        public void run() {
            lock.lock();
            try {
                condition.signal();
                System.out.println("子线程通知");
            } finally {
                lock.unlock();
            }
        }
    }
}

2.4.2、使用Condition实现简单的阻塞队列(生产者消费者)

阻塞队列是一种特殊的先进先出队列,它有以下几个特点
1.入队和出队线程安全
2.当队列满时,入队线程会被阻塞;当队列为空时,出队线程会被阻塞。

  • 代码实现
public class MyBlockingQueue<E> {

    int size;//阻塞队列最大容量

    ReentrantLock lock = new ReentrantLock();

    LinkedList<E> list=new LinkedList<>();//队列底层实现

    Condition notFull = lock.newCondition();//队列满时的等待条件
    Condition notEmpty = lock.newCondition();//队列空时的等待条件

    public MyBlockingQueue(int size) {
        this.size = size;
    }

    public void enqueue(E e) throws InterruptedException {
        lock.lock();
        try {
            while (list.size() ==size)//队列已满,在notFull条件上等待
                notFull.await();
            list.add(e);//入队:加入链表末尾
            System.out.println("入队:" +e);
            notEmpty.signal(); //通知在notEmpty条件上等待的线程
        } finally {
            lock.unlock();
        }
    }

    public E dequeue() throws InterruptedException {
        E e;
        lock.lock();
        try {
            while (list.size() == 0)//队列为空,在notEmpty条件上等待
                notEmpty.await();
            e = list.removeFirst();//出队:移除链表首元素
            System.out.println("出队:"+e);
            notFull.signal();//通知在notFull条件上等待的线程
            return e;
        } finally {
            lock.unlock();
        }
    }
}

2.5、测试可重复锁

package CoreJavaColume.Chapter14;

/**
 * @Author Zhou  jian
 * @Date 2020 ${month}  2020/2/10 0010  17:26
 * 可重入锁“
 *   锁可以重复使用
 */
public class LockTest {

    


    public void test(){
        //第一次获得锁
        synchronized (this){
            while(true){
                //第二次获得同样的锁
                synchronized (this){
                    System.out.println("ReentranLocl");
                }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
            new LockTest().test();
    }


}

2.6、实现不可重复锁

package CoreJavaColume.Chapter14;

/**
 * @Author Zhou  jian
 * @Date 2020 ${month}  2020/2/10 0010  17:29
 * 不可重复锁实现
 */
public class LockTest02 {
    Lock lock = new Lock();

    public void a() throws InterruptedException {
        lock.lock();
        doSomething();
        lock.unlock();
    }
    //不可重入
    public void doSomething() throws InterruptedException {
        lock.lock();
        //....
        lock.unlock();
    }

    public static void main(String[] args) throws InterruptedException {
        LockTest02 test = new LockTest02();
        test.a();
        test.doSomething();
    }

}
//不可重复锁
class Lock{
    //是否占用
    private boolean isLocked = false;

    //使用锁
    public synchronized void lock() throws InterruptedException {
        while(isLocked){
            wait();
        }
        isLocked=true;
    }

    //释放锁
    public synchronized void unlock(){
        isLocked=false;
        notifyAll();
    }

}



2.7、实现可重入锁(ReentranLock的实现)

package CoreJavaColume.Chapter14;

/**
 * @Author Zhou  jian
 * @Date 2020 ${month}  2020/2/10 0010  17:29
 * 可重ru锁实现
 */
public class LockTest02 {
    Lock lock = new Lock();

    public void a() throws InterruptedException {
        lock.lock();
        doSomething();
        lock.unlock();
    }
    //不可重入
    public void doSomething() throws InterruptedException {
        lock.lock();
        //....
        lock.unlock();
    }

    public static void main(String[] args) throws InterruptedException {
        LockTest02 test = new LockTest02();
        test.a();
        test.doSomething();
    }

}
//可重入锁
class Lock{
    //是否占用
    private boolean isLocked = false;
    Thread lockedBy = null;//存储线程
    private int holdCount = 0;


    //使用锁
    public synchronized void lock() throws InterruptedException {
        //被锁住且不是同一个线程锁住
        while(isLocked&&lockedBy!=Thread.currentThread()){
            wait();
        }
        isLocked=true;
        lockedBy=Thread.currentThread();
        holdCount++;
    }

    //释放锁
    public synchronized void unlock(){
        if(Thread.currentThread()==lockedBy){
            holdCount--;
            if(holdCount==0){//当计数器为0
                isLocked=false;
                notifyAll();
                lockedBy=null;
            }
        }
    }

}



3、队列与锁

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oKlfTwSg-1581397703949)(images/13.png)]

每个对象上都有一个

3.0、锁对象

有两种机制防止代码块受并发访问的干扰

Java语言提供了一个synchronized关键字达到这个目的,并引入ReentrantLock类。

synchronized关键字自动提供了一个锁以及相关的条件

  • 锁用来保护代码片段,任何时刻只能有一个线程执行被保护的代码。
  • 锁可以管理视图进入被保护代码段的线程
  • 锁可以拥有一个或多个相关的条件对象
  • 每个条件对象管理那些已经进入被保护的代码段但还不能运行的线程

从上面的例子可以看出,ReentrantLock是可以重入的锁,当一个线程获取锁时,还可以接着重复获取多次。在加上ReentrantLock的的独占性,我们可以得出以下ReentrantLock和synchronized的相同点。

  • 1.ReentrantLock和synchronized都是独占锁,只允许线程互斥的访问临界区。但是实现上两者不同:synchronized加锁解锁的过程是隐式的,用户不用手动操作,优点是操作简单,但显得不够灵活。一般并发场景使用synchronized的就够了;ReentrantLock需要手动加锁和解锁,且解锁的操作尽量要放在finally代码块中,保证线程正确释放锁。ReentrantLock操作较为复杂,但是因为可以手动控制加锁和解锁过程,在复杂的并发场景中能派上用场。
  • 2.ReentrantLock和synchronized都是可重入的。synchronized因为可重入因此可以放在被递归执行的方法上,且不用担心线程最后能否正确释放锁;而ReentrantLock在重入时要却确保重复获取锁的次数必须和重复释放锁的次数一样,否则可能导致其他线程无法获得该锁。

3.1、Synchronized关键字

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3GGCELOV-1581397703949)(images/14.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-spIQ635E-1581397703950)(images/15.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rxyxWunL-1581397703951)(images/16.png)]

锁对象,锁资源

LockCondition接口为程序设计人员提供了高度的锁定控制;然而大多数情况下,并不需要那样的控制,并且可以使用一种嵌入到Java语言内部的机制

Java中的每一个对象都有一个内部锁,如果一个方法用synchronized关键字声明,那么对象的锁将保护整个方法。也就是说,要调用该方法,线程必须获得内部的锁对象

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nHFAY7SN-1581397703952)(images/32.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VGYQUxxc-1581397703952)(images/17.png)]

3.1.1、Synchronized方法

改进Web12306

package CoreJavaColume.Chapter14;

/**
 * @Author Zhou  jian
 * @Date 2020 ${month}  2020/2/8 0008  15:14
 * 线程安全:在并发时保证数据的正确性,效率尽可能高
 * synchronized:
 *    1、同步方法
 *    2、同步块
 */
public class SafetEST01 {

    public static void main(String[] args) {
       SafeWeb12306 web = new SafeWeb12306();

        //多个代理
        new Thread(web,"吗").start();
        new Thread(web,"吗农").start();
        new Thread(web,"吗皇").start();
    }
}
class SafeWeb12306  implements  Runnable{

    //票数
    private int ticketNums = 10;
    private boolean flag = true;

    @Override
    public void run() {
        while (flag) {
            test();
        }
    }

    public synchronized  void test(){//锁的是资源:this
        if(ticketNums<0) {flag=false; return;}
        //模拟延时
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"---->"+ticketNums--);
    }

}


账户改进

账户锁定失败0

package CoreJavaColume.Chapter14;

/**
 * @Author Zhou  jian
 * @Date 2020 ${month}  2020/2/8 0008  15:21
 * 
 * 锁失败 :这里锁的应该是 账户
 */
public class SafeTest02 {

    public static void main(String[] args) {
                //账户
        Account account = new Account(100,"结婚礼金");

        Drawing you = new Drawing(account,80,"可悲的你");
        Drawing wife = new Drawing(account,90,"happy的它");

        you.start();//启动线程  开始取钱
        wife.start();
    }
}

class Account{
    int money;
    String name;

    public Account(int money, String name) {
        this.money = money;
        this.name = name;
    }
}

//模拟取款
class Drawing extends  Thread{

    Account account;//取钱的账户
    int drawingMoney;//取的钱数
    int packetTotal;//取的总数


    public Drawing(Account account, int drawingMoney,String name) {
        super(name);
        this.account = account;
        this.drawingMoney = drawingMoney;
    }

    @Override
    public void run() {
        test();
    }


    public  synchronized void  test(){

        //账户余额不够
        if(account.money-drawingMoney<0) return;

        //模拟网络延时
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        account.money-=drawingMoney;
        packetTotal +=drawingMoney;
        System.out.println(this.getName()+"---->账户余额为:"+account.money);
        System.out.println(this.getName()+"---->口袋的前为:"+packetTotal);
    }
}

3.1.2、Synchronized同步块

解决上面锁定银行账户的问题。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ul6S2bDx-1581397703953)(images/18.png)]

obj是锁住的对象

锁账户

package CoreJavaColume.Chapter14;

/**
 * @Author Zhou  jian
 * @Date 2020 ${month}  2020/2/8 0008  15:21
 *
 * 锁失败 :这里锁的应该是 账户
 */
public class SafeTest02 {

    public static void main(String[] args) {
                //账户
        Account account = new Account(100,"结婚礼金");

        Drawing you = new Drawing(account,80,"可悲的你");
        Drawing wife = new Drawing(account,90,"happy的它");

        you.start();//启动线程  开始取钱
        wife.start();
    }
}

class Account{
    int money;
    String name;

    public Account(int money, String name) {
        this.money = money;
        this.name = name;
    }
}

//模拟取款
class Drawing extends  Thread{

    Account account;//取钱的账户
    int drawingMoney;//取的钱数
    int packetTotal;//取的总数


    public Drawing(Account account, int drawingMoney,String name) {
        super(name);
        this.account = account;
        this.drawingMoney = drawingMoney;
    }

    @Override
    public void run() {
        test();
    }


    public   void  test() {

        //同步块,目标锁定块
        synchronized (account) {//锁定账户;区别于刚才锁的方法
            //账户余额不够
            if (account.money - drawingMoney < 0) return;

            //模拟网络延时
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            account.money -= drawingMoney;
            packetTotal += drawingMoney;
            System.out.println(this.getName() + "---->账户余额为:" + account.money);
            System.out.println(this.getName() + "---->口袋的前为:" + packetTotal);
        }
    }
}

容器中添加数据的问题

package CoreJavaColume.Chapter14;

import java.util.ArrayList;
import java.util.List;

/**
 * @Author Zhou  jian
 * @Date 2020 ${month}  2020/2/9 0009  22:46
 */
public class SafeTest03 {

    public static void main(String[] args) throws InterruptedException {

        List<String> list = new ArrayList<>();
        //开启10000个线程
        for(int i=0;i<10000;i++){
            synchronized (list){
            new Thread(()->{list.add(Thread.currentThread().getName());}).start();}
        }
        Thread.sleep(10000);
        System.out.println(list.size());


    }
}

3.1.3、锁的粒度小,提升性能

  • 范围太小,锁不住
  • 范围太大,性能效率低下
  • 多用sychronized块
 @Override
    public void run() {
        while (flag) {
            test();
        }
    }
    //synchronized在锁的时候,它的内容可以改变;但是它的地址不能改变
    //尽可能锁定合力的范围(不是指代码 指数据的完整性)
    //双重检测:主要确定临界值的问题
    public   void test() {

        if (ticketNums < 0) { //考虑没有票的情况
            flag = false;
            return;
        }

        synchronized (this) {

            if (ticketNums < 0) { //考虑最后一张票
                flag = false;
                return;
            }

            //模拟延时
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "---->" + ticketNums--);
        }

3.1.4、共享例子

快乐电影院

package CoreJavaColume.Chapter14;

import com.sun.org.apache.bcel.internal.generic.NEW;

import java.util.ArrayList;
import java.util.List;

/**
 * @Author Zhou  jian
 * @Date 2020 ${month}  2020/2/9 0009  22:59
 * 快乐影院
 */
public class HappyCinema {

    public static void main(String[] args) {
        ArrayList<Integer> available = new ArrayList<>();
        available.add(1);
        available.add(2);
        available.add(3);
        available.add(6);
        available.add(7);

        //顾客需要的位置
        List<Integer> seats1 = new ArrayList<>();
        seats1.add(1);
        seats1.add(2);

        List<Integer> seats2 = new ArrayList<>();
        seats2.add(3);
        seats2.add(7);
        seats2.add(6);


        Cinema cinema = new Cinema(available,"happy");
        new Thread(new Customer(cinema,seats1),"战三").start();
        new Thread(new Customer(cinema,seats2),"李四").start();
    }



}

//顾客:有多个顾客
class Customer implements Runnable{
    Cinema cinema;
    List<Integer> seats;

    public Customer(Cinema cinema, List<Integer> seats) {
        this.cinema = cinema;
        this.seats = seats;
    }

    @Override
    public void run() {
        //锁的块;锁共享资源cinemal
        synchronized (cinema) {
            boolean flag = cinema.bookTickets(seats);
            if (flag) {
                System.out.println("出票成功" + Thread.currentThread().getName() + "位置为" + seats);
            } else {
                System.out.println("出票失败" + Thread.currentThread().getName() + "位置不够");
            }
        }
    }
}



//影院
class Cinema{

    List<Integer> available;//可用的位置
    String name;//名称

    public Cinema( List<Integer> available, String name) {
        this.available = available;
        this.name = name;
    }


    //购票
    public boolean bookTickets(List<Integer> seats){
        System.out.println("可用位置"+available);
        List<Integer> copy = new ArrayList<>();
        copy.addAll(available);

        //减
        copy.removeAll(seats);
        //判断大小
        if(available.size()-copy.size()!=seats.size()){
            return false;
        }else{
                //成功
                available=copy;
                return true;
        }
    }

}


3.2、死锁

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vHrJDz8f-1581397703954)(images/19.png)]

package CoreJavaColume.Chapter14;

/**
 * @Author Zhou  jian
 * @Date 2020 ${month}  2020/2/10 0010  00:39
 * 死锁:过多的同步可能造成资源相互不释放
 * 从而等待,一般发生于同步等待中持有多个对象的锁
 *
 */
public class DeadLock {

    public static void main(String[] args) {
                Markup g1 = new Markup(1,"狛枝");
                Markup g2 = new Markup(0,"王菲");

                g1.start();
                g2.start();
    }
}
//口红
class LipStick{


}

//镜子
class Mirror{


}

//划转
class Markup extends Thread{

    //选择
    int choice;
    //名字
    String girl;
    static  LipStick lipStick = new LipStick();
    static  Mirror mirror = new Mirror();


    public Markup(int choice,String girl) {
        this.choice = choice;
        this.girl=girl;
    }

    @Override
    public void run() {
        try {
            markup();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


    //相互持有对方的对象锁->可能造成死锁
    private void markup() throws InterruptedException {

        if(choice==0){
            synchronized (lipStick){//或的口红的锁
                System.out.println(this.girl+"获得口红");
            Thread.sleep(1000);
            synchronized (mirror){
                System.out.println(this.girl+"获得镜子");
            }

        }            }
        else{

            synchronized (mirror){//或的口红的锁
                System.out.println(this.girl+"获得镜子");

            Thread.sleep(2000);
            synchronized (lipStick){
                System.out.println(this.girl+"获得口红");
            }


        }
        }

    }
}

3.3、单例模式

  • Synchronizedvolatile实现单例模式
package CoreJavaColume.Chapter14;

/**
 * @Author Zhou  jian
 * @Date 2020 ${month}  2020/2/10 0010  17:01
 *
 * 单例模式:懒汉式套路基础上  在多线程环境下,对外存在一个对象
 * 1、构造器私有化--->避免外部new
 * 2、内部提供私有的静态属性 -->存储对象的地址
 * 3、提供公共的静态方法-->获取属性
 *
 */
public class DobuleCheckedLocking {

    //2、提供私有的静态属性
    //voltailE避免指令重拍
    //没有可能导致访问没有初始化对象
    private static  volatile  DobuleCheckedLocking instance;


    public static void main(String[] args) {

        DobuleCheckedLocking instance1 = DobuleCheckedLocking.getInstance();
        DobuleCheckedLocking instance2 = DobuleCheckedLocking.getInstance();
        System.out.println(instance1==instance2);

    }

    //2、构造器私有化
    private DobuleCheckedLocking(){



    }

    //3、提供公共的静态方法--》获取属性
    public  static DobuleCheckedLocking getInstance() {
        //再次检测
        if(instance!=null){//避免不必要的同步,已经存在对象
            return instance;
        }
        synchronized (DobuleCheckedLocking.class) {
            if (null == instance) {
                instance = new DobuleCheckedLocking();
                //开空间
                //初始化对象信息
                //返回对象地址给引用
            }

        }
        return instance;
    }


}

4、Volatile关键字(不多见)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HAIwFt3f-1581397703955)(images/27.png)]

volatile 关键字为实例域的同步访问提供了一种免锁机制。如果声明一个域为 volatile ,
那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的。


package CoreJavaColume.Chapter14;

/**
 * @Author Zhou  jian
 * @Date 2020 ${month}  2020/2/10 0010  16:56
 * volatile用于保证数据的同步,也就是可见性
 */
public class VolatileTest01 {

    private  volatile  static int num = 0;

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            while(num==0){//此处不要编写代码

            }
        }).start();

        Thread.sleep(10000);

        num=1;

    }
}

5、ThreadLocal

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sjYjE7bM-1581397703955)(images/28.png)]

每个线程有自己的空间

package CoreJavaColume.Chapter14;

/**
 * @Author Zhou  jian
 * @Date 2020 ${month}  2020/2/10 0010  17:14
 * ThreadLocal:每个线程自身的存储本地、局部区域
 * get/set/intgialValue
 */
public class ThreadLocalTest01 {

        private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        //更改初始值

        System.out.println(Thread.currentThread().getName()+"---->"+threadLocal.get());

        //设置值
        threadLocal.set(99);
        System.out.println(Thread.currentThread().getName()+"---->"+threadLocal.get());

        new Thread(new MyRun()).start();

    }

    public static class MyRun implements Runnable{

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+"---->"+threadLocal.get());
            threadLocal.set(100);
            System.out.println(Thread.currentThread().getName()+"---->"+threadLocal.get());
        }
    }


}

6、原子性

java.util.concurrent.atomic包中有很多类使用了很高效的及其指令(而不是使用锁)来保证其他操作的原子性。

例如,AtomicInteger类提供了方法incrementAndGetdecrementAndGet,他们分别以原子方式将一个整数自增和自减。

例如,可以安全的生成一个数值序列,如下所示:

public static AtomicLong nextNumber = new AtomicLong();
//In some thread.....
long id = nextNumber.incrementAndGet();

IncrementAndGet方法以原子方式将AtomicLong自增,并返回自增后的值。也就是说,获得值、增1并设置然后生成新值的操作不会中断。可以保证即使多个线程并发的访问同一个实例,也会计算并返回正确的值

7、锁的分类

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hxjE0ZKb-1581397703956)(images/30.png)]

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!