线程同步

并发,同一个对象被多线程同时操作

现实生活中,我们会遇到同一个资源,很多人都想使用的问题,比如:食堂排队打饭,每个人都想吃饭,最天然的方法就是排队,一个人一个人来

处理多线程的问题时,多个线程访问同一个对象,并且某些线程还想修改这个对象,这个时候就需要线程同步,线程同步就是一种等待机制,多个需要访问此对象的线程进入对象的等待池,形成队列,等待前面线程使用完毕,下一个线程再使用

由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问的冲突问题,为了保证数据在方法中被访问的正确性,在访问时加入了锁机制,synchronized,当一个线程获得对象的排他锁,独占资源,其他线程必须等待,使用后释放即可。

但是也会存在以下问题:
1)一个线程持有锁,会导致其他所有需要此锁的线程挂起
2)在多线程情况下,加锁,释放锁,会导致比较多的上下文切换和调度延时,引起性能问题
3)如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能问题

线程不安全案例

第一个案例:买票

package syn;

/**
 * @Classname UnsafeBuyTicket
 * @Description TODO
 * @Date 2020/12/8 11:12
 * @Created by mmz
 */
/*不安全的买票的方法*/
public class UnsafeBuyTicket {
    public static void main(String[] args) {
        BuyTicket buyTicket = new BuyTicket();
        new Thread(buyTicket,"a").start();
        new Thread(buyTicket,"b").start();
        new Thread(buyTicket,"c").start();
    }
}

class BuyTicket implements Runnable{
    private int ticket = 5;
    boolean flag = true;
    @Override
    public void run() {
        // 买票
        while(flag){
            buy();
        }
    }

    private void buy(){
        // 判断是否有票
        if(ticket <=0 ){
            flag = false;
            return;
        }
        System.out.println(Thread.currentThread().getName()+ "拿到" +ticket--);
    }
}

第二个案例:银行取钱

package syn;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @Classname UnsafeBank
 * @Description TODO
 * @Date 2020/12/8 12:42
 * @Created by mmz
 */
// 不安全的取钱
public class UnsafeBank {
    public static void main(String[] args) {
        Account account = new Account(100,"结婚基金");
        Drawing you = new Drawing(account,50,"你");
        Drawing she = new Drawing(account,100,"zhong");
        you.start();
        she.start();
        ReentrantLock lock = new ReentrantLock();
    }
}

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

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

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+  "线程开始");
        if((account.money - drawingMoney) < 0){
            System.out.println(Thread.currentThread().getName() + "钱不够,取不了");
            return;
        }

        try {
            if(Thread.currentThread().getName().equals("你")){
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+ "线程执行了");
        account.money =account.money-drawingMoney;
        nowMoney += drawingMoney;
        System.out.println(account.name +"余额为"  + account.money);
        System.out.println(this.getName() + "手里的钱" + this.nowMoney);

    }
}

第三个案例:jdk中的ArrayList

package syn;

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

/**
 * @Classname UnsafeList
 * @Description TODO
 * @Date 2020/12/8 15:05
 * @Created by mmz
 */
/*线程不安全的list*/
public class UnsafeList {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        System.out.println(list.size()); // 结果应该小于1000
    }
}

线程同步的方法

由于我们可以用关键字private来保证数据对象只能被方法访问,所以我们需要针对方法提出一套机制,这套机制就是synchronized关键字,它包含两种方法:sychronized方法和synchronized块

synchronized方法控制对象的访问,每个对象就相当于一把锁,每个synchronized方法必须都必须调用该方法的对象的锁才能执行,否则线程会阻塞,方法一旦执行,就能独占该锁,直到该方法返回才释放锁,后面被阻塞的线程才能获得这个锁,继续执行

修改第一个案例:买票

package syn;

/**
 * @Classname SafeBuyTicket
 * @Description TODO
 * @Date 2020/12/8 15:14
 * @Created by mmz
 */
public class SafeBuyTicket {
    public static void main(String[] args) {
        BuySafeTicket buySafeTicket = new BuySafeTicket();
        new Thread(buySafeTicket,"a").start();
        new Thread(buySafeTicket,"b").start();
        new Thread(buySafeTicket,"c").start();
    }
}
class BuySafeTicket implements Runnable{
    private int ticket = 1000;
    boolean flag = true;
    @Override
    public void run() {
        // 买票
        while(flag){
            buy();
        }
    }

    // 同步方法,锁的是this
    private  synchronized void buy(){
        // 判断是否有票
        if(ticket <=0 ){
            flag = false;
            return;
        }
        System.out.println(Thread.currentThread().getName()+ "拿到" +ticket--);
    }
}

修改第二个案例:取钱

package syn;

import java.util.concurrent.locks.ReentrantLock;

/**
 * @Classname SafeBank
 * @Description TODO
 * @Date 2020/12/8 15:22
 * @Created by mmz
 */
public class SafeBank {
    public static void main(String[] args) {
        AccountSafe accountSafe = new AccountSafe(100,"结婚基金");
        DrawingSafe me = new DrawingSafe(accountSafe,50,"你");
        DrawingSafe he = new DrawingSafe(accountSafe,100,"zhong");
        me.start();
        he.start();
    }

}
class AccountSafe{
    int money; // 余额
    String name; // 卡名
    public AccountSafe(int money,String name){
        this.money = money;
        this.name = name;
    }
}

class DrawingSafe extends Thread{
    AccountSafe accountSafe;
    int drawingMoney;
    int nowMoney;

    public DrawingSafe(AccountSafe accountSafe,int drawingMoney,String name){
        super(name);
        this.accountSafe = accountSafe;
        this.drawingMoney = drawingMoney;
    }

    @Override
    public  void run() {

        synchronized (accountSafe){
            System.out.println(Thread.currentThread().getName()+  "线程开始");
            if((accountSafe.money - drawingMoney) < 0){
                System.out.println(Thread.currentThread().getName() + "钱不够,取不了");
                return;
            }

            try {
                if(Thread.currentThread().getName().equals("你")){
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+ "线程执行了");
            accountSafe.money =accountSafe.money-drawingMoney;
            nowMoney += drawingMoney;
            System.out.println(accountSafe.name +"余额为"  + accountSafe.money);
            System.out.println(this.getName() + "手里的钱" + this.nowMoney);

        }

    }
}

修改案例三:jdk中的ArrayList

package syn;

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

/**
 * @Classname SafeList
 * @Description TODO
 * @Date 2020/12/8 15:32
 * @Created by mmz
 */
public class SafeList {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            new Thread(()->{
                synchronized (list) {
                    list.add(Thread.currentThread().getName());
                }
            }).start();
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(list.size()); // 结果应该小于1000
    }
}

同步块

synchronized(obj){}
obj称为同步监视器,其中obj可以是任何一个对象,但是推荐使用同步资源来作为监视器。
同步方法中无需指定同步监视器,因为同步方法的同步监视器就是this,就是这个对象本身就是class

同步监视器的执行过程

1)第一个线程执行,锁定同步监视器,执行其中代码
2)第二个线程访问,发现同步监视器被锁定,无法访问
3)第一个线程访问完毕,解锁同步监视器
4)第二个线程访问,发现同步监视器没有锁,然后锁定并且访问

juc下面的CopyOnWriteList

package juc;

import java.util.concurrent.CopyOnWriteArrayList;

/**
 * @Classname TestJUC
 * @Description TODO
 * @Date 2020/12/8 15:37
 * @Created by mmz
 */
public class TestJUC {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> copyOnWriteArrayList = new CopyOnWriteArrayList();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                copyOnWriteArrayList.add(Thread.currentThread().getName());
            }).start();
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(copyOnWriteArrayList.size());
    }
}

死锁

多个线程各自占有一些共享资源,并且相互等待其他线程占有的资源才能运行,而导致两个或者多个线程都在等待对方释放的资源,都停止执行的情形,某一个同步块中拥有两个对象以上的锁,就可能发生这样的事情。

自己写的一个死锁的案例

package deadlock;

/**
 * @Classname TestDeadLock
 * @Description TODO
 * @Date 2020/12/8 15:47
 * @Created by mmz
 */
public class TestDeadLock {
    public static void main(String[] args) {
        Object locka = new Object();
        Object lockb = new Object();

        AThread aThread = new AThread(locka,lockb);
        BThread bThread = new BThread(locka,lockb);

        new Thread(aThread,"a").start();
        new Thread(bThread,"b").start();
    }
}

class AThread implements Runnable{
    private Object locka;
    private Object lockb;
    AThread(Object locka,Object lockb){
        this.locka = locka;
        this.lockb = lockb;
    }
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "启动了");
        synchronized (locka){
            System.out.println(Thread.currentThread().getName() + "获取了a的锁");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "准备获取b的锁");
            synchronized (lockb){
                System.out.println("获取b的锁了");
            }
        }
    }
}

class BThread implements Runnable{
    private Object locka;
    private Object lockb;
    BThread(Object locka,Object lockb){
        this.locka = locka;
        this.lockb = lockb;
    }
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "启动了");
        synchronized (lockb){
            System.out.println(Thread.currentThread().getName() + "获取了b的锁");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "准备获取a的锁");
            synchronized (locka){
                System.out.println("获取a的锁了");
            }
        }
    }
}

程序一直运行中,两个线程互相需要对方的资源

产生死锁的四大条件

1)互斥
2)持有并且等待
3)不可被剥夺
4)循环等待

想要解决死锁的问题,就从上面四个方面去突破

Lock(锁)

从jdk5.0开始,java提供了更强大的线程同步机制,通过显示定义同步锁对象来实现同步,同步锁使用Lock对象充当

java.util.concurrent.locks.Lock 接口是控制多个线程对共享资源进行访问的工具,锁提供了对共享资源的独占访问,每次只能有一个线程对lock对象加锁,线程开始访问共享资源之前首先需要获取lock对象

ReentranLock类实现了Lock类,它拥有与Synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentranLock,可以显示的加锁,释放锁。

package lock;

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

import java.util.concurrent.locks.ReentrantLock;

/**
 * @Classname TestLock
 * @Description TODO
 * @Date 2020/12/8 17:02
 * @Created by mmz
 */
public class TestLock {
    public static void main(String[] args) {
        TestLock2 testLock2 = new TestLock2();

        new Thread(testLock2).start();
        new Thread(testLock2).start();
        new Thread(testLock2).start();
    }



}

class TestLock2 implements Runnable{
    int tickets = 10;
    // 定义lock
    private final ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {
        while (true){
            try {
                lock.lock();
                if(tickets>0){
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(tickets--);
                }else{
                    break;
                }
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                lock.unlock();
            }


        }
    }
}

lock是显示锁,(手动开启和关闭),synchronized是隐式锁,出了作用域自动释放

lock只有代码块锁,synchronized有代码块和方法锁
使用lock锁,jvm将花费较少的时间来调度线程,性能更好。并且具有更好的拓展性(提供更多的子类)