线程的同步和锁的概念

时间:2021-04-21 12:13:16   收藏:0   阅读:0

并发

线程不安全测试

买票

public class Test {
    public static void main(String[] args) {
        //一份资源
        MyThread myThread=new MyThread();
        //多个代理
        new Thread(myThread,"李斯").start();
        new Thread(myThread,"张良").start();
        new Thread(myThread,"韩非").start();
    }
}
class MyThread  implements  Runnable{
    private int num=10;//剩余的火车票数
    boolean flag=true;

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

结果

韩非-->10
张良-->9
李斯-->8
韩非-->7
张良-->6
李斯-->5
韩非-->4
李斯-->3
张良-->2
韩非-->1
李斯-->0
张良-->-1
结果分析
上面的结果出现了负数:
  Thread.sleep(200);
  假设出现abc三个人,当只有一张票的时候,a最先获得时间片,抢到了1,b刚进入的时候1还没又来得及修改,因此继续使用资源,但经过sleep()后获得的资源已被修改变成了0,以此类推c只能抢到-1
  
第二次运行
李斯-->10
韩非-->10
张良-->9
张良-->8
李斯-->7
韩非-->6
韩非-->5
李斯-->4
张良-->3
韩非-->2
李斯-->1
张良-->1
韩非-->0
张良-->-1
结果分析:
上面的结果出现了相同的数10
开辟多线程后多线程都有自己的工作空间,ABC都有自己的工作空间,这些空间都与主内存进行交换。
A从主内存将10拷贝过来后,10还没来得及修改(-1的操作)就被B从主内存拷贝到了自己的内存空间,所以就导致了两个人抢到了同一张票。

上面两种情况就导致了线程不安全。

线程不安全二

集合示例:

public class Test {
    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());
    }

}

结果

554
分析:实际长度本应该是1000,但是这次运行的运行结果显示的实际长度却是554,
这是因为线程在运行的时候发生了覆盖,如当线程的名相同的时候,就可以覆盖前面那个同名的线程。
所以此处的测试也导致了线程不安全。

提示

1. 不是所有的线程都需要线程安全
2. 通常情况下,读不需要线程安全,发生改的操作就需要线程安全,又读又改需要线程安全。

线程不安全测试三

模拟父亲和儿子同时取一张卡的钱

public class Test {
    public static void main(String[] args) {
        //账户
        Account account=new Account(100,"生活费");
        Drawing you=new Drawing(account,80,"儿子");
        Drawing rent=new Drawing(account,90,"父亲");
        you.start();
        rent.start();
    }
}
//模拟取款
class Drawing extends  Thread {
    Account account;//取钱的账户
    int drawingMoney;//取钱数
    int packetTotal;//身上还有多少钱
    public Drawing(Account account,int drawingMoney,String name){
        super(name);// thread线程名字
        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("取钱后账户余额为:"+account.money);
        System.out.println(this.getName()+"取了多少钱:"+packetTotal);
    }
}
class Account{
    int money;//金额
    String name;//名称

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

结果

取钱后账户余额为:-70
父亲取了多少钱:90
取钱后账户余额为:-70
儿子取了多少钱:80

分析:经过Thread.sleep(1000);后我们发现出现了线程不安全,当然睡眠不是造成线程不安全的原因,当多启动几个线程同时共享资源就算不使用睡眠也可能出现线程不安全。此处使用睡眠只是方便测试。在一个线程睡眠时,账户余额还没来得及变化,另一个线程访问没有来得及变化的余额满足了条件,最后执行完毕却使用了变化后的账户,使得最后账户变为了负数,造成了线程不安全。

synchronized方法

package com.dongjixue.test;

public class Test {
    public static void main(String[] args) {
        //一份资源
        MyThread myThread=new MyThread();
        //多个代理
        new Thread(myThread,"李斯").start();
        new Thread(myThread,"张良").start();
        new Thread(myThread,"韩非").start();
    }
}
class MyThread  implements  Runnable{
    private int num=10;//剩余的火车票数
    boolean flag=true;
	//线程安全(同步)
    //下面的同步方法可以改为同步块,参数传递this,这样做可以提升性能。
    //(可以直接锁,因为num属于this.class)
    public synchronized void test(){
        if(num<=0){
            flag=false;
            return;
        }
        //模拟网络延时
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"-->"+num--);
    }
    @Override
    public void run() {
        while (flag){
            test();
        }
    }
}

结果

李斯-->10
李斯-->9
韩非-->8
张良-->7
韩非-->6
李斯-->5
韩非-->4
张良-->3
韩非-->2
李斯-->1

分析:和线程不安全测试进行对比,加入锁和保证了数据的准确性,同时将使用的资源封装到test方法中,而不是将资源直接写到run方法里面(锁对地方),这样做的目的是尽可能的提高性能。

测试二 取钱

public class Test {
    public static void main(String[] args) {
        //账户
        Account account=new Account(100,"生活费");
        Drawing you=new Drawing(account,80,"儿子");
        Drawing rent=new Drawing(account,90,"父亲");
        you.start();
        rent.start();
    }
}
//模拟取款
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();
    }
    //此处的this是指Drawing.class
    //而我们修改的是Account中的account,所以没有锁对对象。还是会出错
    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("取钱后账户余额为:"+account.money);
        System.out.println(this.getName()+"取了多少钱:"+packetTotal);
    }
}
class Account{
    int money;//金额
    String name;//名称

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

结果

取钱后账户余额为:-70
取钱后账户余额为:-70
儿子取了多少钱:80
父亲取了多少钱:90

分析:结果还是出现了异常,这是因为没有锁对资源造成的。我们应该锁定的是Account的资源,而不是test这个方法。

synchronized块

取钱示例:

package com.dongjixue.test;

public class Test {
    public static void main(String[] args) {
        //账户
        Account account=new Account(100,"生活费");
        Drawing you=new Drawing(account,80,"儿子");
        Drawing father=new Drawing(account,10,"父亲");
        Drawing mother=new Drawing(account,100,"母亲");
        you.start();
        father.start();
        mother.start();
    }
}
//模拟取款
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(){
        //当账户没有钱就不进入同步块,直接结束方法,节约了资源,提升了性能
        if (account.money<=0){
            return;
        }
        //指定要锁的对象,这样就不会锁错对象
        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("取钱后账户余额为:"+account.money);
            System.out.println(this.getName()+"取了多少钱:"+packetTotal);
        }

    }
}
class Account{
    int money;//金额
    String name;//名称

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

结果

取钱后账户余额为:20
儿子取了多少钱:80
取钱后账户余额为:10
父亲取了多少钱:10

结果正确
提示:要锁对资源

测试二 集合

package com.dongjixue.test;

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

public class Test {
        public static void main(String[] args) throws InterruptedException {
            List<String> list=new ArrayList<>();
            for (int i = 0; i < 1000; i++) {
                new Thread(()->{
                    synchronized (list){
                        list.add(Thread.currentThread().getName());
                    }
                }).start();
            }
            //延时5秒才执行下面的输出语句,不然当上面的线程还没执行完毕
            //就执行到了下面的输出语句,也会造成结果不正确。
            Thread.sleep(5000);
            System.out.println(list.size());
        }
}

结果:正确

1000

容器的自带锁(juc编程)

public class Test {
        public static void main(String[] args) throws InterruptedException {
           CopyOnWriteArrayList <String> list=new CopyOnWriteArrayList<>();
            for (int i = 0; i < 1000; i++) {
                new Thread(()->{
                        list.add(Thread.currentThread().getName());
                }).start();
            }
            Thread.sleep(5000);
            System.out.println(list.size());
        }
}

结果

1000

死锁

示例:两个人同时吃一碗饭和喝一碗酒

public class Test {
        public static void main(String[] args) {
           People people=new People(0,"小红");
           people.start();
           People people2=new People(1,"小明");
           people2.start();
        }
}
//喝酒
class Drink{

}
//吃饭
class Eat{

}
class People extends Thread{
    int choice; //选择喝酒还是吃饭
    String name;

    //static表示饭和酒只有一份,不写表示多份,有多份资源下面就不会产生死锁
   	static Eat eat=new Eat();
    static Drink drink=new Drink();

    public People(int choice, String name) {
        this.choice = choice;
        this.name = name;
    }

    @Override
    public void run() {
        //吃饭和喝酒
        doWhat();
    }
    //先后持有对方的对象锁->可能造成死锁
    private void doWhat(){
        if(choice==0){
            //获得喝酒的锁
            synchronized (drink){
                System.out.println(this.name+"喝酒");
                //1秒后想获得吃饭的锁
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (eat){
                    System.out.println(this.name+"吃饭");
                }
            }
        }else {
            //获得吃饭的锁
            synchronized (eat){
                System.out.println(this.name+"吃饭");
                //2秒后想获得喝酒的锁
                try {
                    Thread.sleep(4000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (drink){
                    System.out.println(this.name+"喝酒");
                }
            }
        }
    }
}

结果

小红喝酒
小明吃饭

//分析结果:
小明吃饭后该喝酒,小红喝酒后该吃饭。但小明占据了饭这一份资源,小红占据了酒这一份资源。它们都想要等待对方释放资源,结果就造成了死锁。

修改示例

public class Test {
        public static void main(String[] args) {
           People people=new People(0,"小红");
           people.start();
           People people2=new People(1,"小明");
           people2.start();
        }
}
//喝酒
class Drink{

}
//吃饭
class Eat{

}
class People extends Thread{
    //选择喝酒还是吃饭
    int choice;
    String name;

    //static表示饭和酒只有一份
   static Eat eat=new Eat();
    static Drink drink=new Drink();

    public People(int choice, String name) {
        this.choice = choice;
        this.name = name;
    }

    @Override
    public void run() {
        //吃饭和喝酒
        doWhat();
    }
    //先后持有对方的对象锁->可能造成死锁
    private void doWhat(){
        if(choice==0){
            //获得喝酒的锁
            synchronized (drink) {
                System.out.println(this.name + "喝酒");
                //1秒后想获得吃饭的锁
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //喝完酒后,释放酒的锁再持有饭的锁就不会造成死锁
            synchronized (eat){
               System.out.println(this.name+"吃饭");
            }
        }else {
            //获得吃饭的锁
            synchronized (eat){
                System.out.println(this.name+"吃饭");
                //2秒后想获得喝酒的锁
                try {
                    Thread.sleep(4000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            synchronized (drink){
                System.out.println(this.name+"喝酒");
            }
        }
    }
}

结果

小红喝酒
小明吃饭
小明喝酒
小红吃饭

分析:当我们将锁移除另一个锁,不要锁套索。结果就正确了,当它们用完资源就能够释放,让另一个线程(小红/小明)去使用它们各自释放的资源。

参考教材:尚学堂Java300集

评论(0
© 2014 mamicode.com 版权所有 京ICP备13008772号-2  联系我们:gaon5@hotmail.com
迷上了代码!