码迷,mamicode.com
首页 > 编程语言 > 详细

线程的同步和锁的概念

时间:2021-04-21 12:13:16      阅读:0      评论:0      收藏:0      [点我收藏+]

标签:win   必须   集合   一个   rup   pac   raw   异常   锁定   

  • 线程的同步是指在一个多线程下,我们要保证数据的准确性和安全性,同时还要提高它的性能。

并发

  • 并发是指同一个对象被多个线程同时操作。

    1. 买火车票:多个人同时买一张火车票
    2. 同一个银行账户,一个人在柜台取钱的同时,另一个人在机器上面取钱。

    上面的两个例子会导致线程不安全

线程不安全测试

买票

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

  • 现时生活中,我们会面临一个资源,多个人都想使用的问题。解决方法当然就是排队。

  • 处理多线程的时候,多个线程访问同一个对象,并且还需要修改这个对象,这时我们就需要用到线程同步

  • 线程同步起始就是一种等待机制,多个需要访问同一个对象的线程进入对象的等待池形成队列。等待前面的线程使用完毕,下一个线程再使用。

  • 使用资源的线程将资源锁定,此时只有它自己能够使用,直到结束,下一个再进行锁定,使用资源。

  • 由于同一个进程的多个线程共享同一块存储空间,会造成访问冲突的问题,因此为了保护访问时的正确性,我们在访问的时候加入了锁机制

  • 当线程获得了锁机制就能独占资源,其它线程必须等待。

  • 锁机制带来的问题

    1. 一个线程使用锁,会导致其它需要此锁的线程程挂起。
    2. 多线程的竞争下,频繁的加锁和释放锁会造成较多的上下文切换和调度延时,引起性能问题。
    3. 一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题。(理解:让执行时间少的等待执行时间长的)
  • 锁一个具体的对象

    1. 成员方法锁this

    2. 静态方法锁class对象

    3. 锁会在两个地方出现:

      a. 方法(同步方法):synchronized 方法

      b. 块(同步块):synchronized 块

    4. 声明synchronized后会大大的影响效率。

synchronized方法

  • 线程安全时要保证数据的准确性,和效率尽可能的高
  • 成员方法锁的是this,是对象而不是指锁了这个方法(因为这些方法和一些属性都属于这个对象)。(要找对锁的对象,修改那个属性,就锁拥有那个属性的对象)
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方法里面(锁对地方),这样做的目的是尽可能的提高性能。

测试二 取钱

  • 注意:要锁定正确的对象
    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);
        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编程)

  • 容器自带锁已经实现了锁,我们可以直接使用,而不用去手动的给容器增加锁
  • 使用方法将List集合该外CopyOnWriteArrayList。
  • CopyOnWriteArrayList对操作的数据加了锁的操作
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集

线程的同步和锁的概念

标签:win   必须   集合   一个   rup   pac   raw   异常   锁定   

原文地址:https://www.cnblogs.com/lanxinren/p/14678316.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!