Java基础进阶多线程-线程安全和synchronized关键字

导读:本篇文章讲解 Java基础进阶多线程-线程安全和synchronized关键字,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com


关于多线程并发环境下,数据的安全问题。

为什么线程安全这个是重点

以后在开发中,我们的项目都是运行在服务器当中,
而服务器已经将线程的定义,线程对象的创建,线程
的启动等,都已经实现完了。这些代码我们都不需要
编写。

最重要的是:你要知道,你编写的程序需要放到一个
多线程的环境下运行,你更需要关注的是这些数据
在多线程并发的环境下是否是安全的(重点:*****)

什么时候数据在多线程并发的环境下会存在安全问题呢?

三个条件:

  • 条件1:多线程并发。
  • 条件2:有共享数据
  • 条件3:共享数据有修改的行为。

满足以上3个条件之后,就会存在线程安全问题。

怎么解决线程安全问题呢?

当多线程并发的环境下,有共享数据,并且这个数据还会被修改,此时就存在
线程安全问题,怎么解决这个问题?

  • 线程排队执行。(不能并发)。
  • 用排队执行解决线程安全问题。
  • 这种机制被称为:线程同步机制。

专业术语叫做:线程同步,实际上就是线程不能并发了,线程必须排队执行

  • 使用“线程同步机制”。

  • 线程同步就是线程排队了,线程排队了就会牺牲一部分效率,没办法,数据安全
    第一位,只有数据安全了,我们才可以谈效率。数据不安全,没有效率的事儿。

说到线程同步这块,涉及到这两个专业术语:

异步编程模型:

  • 线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,
    谁也不需要等谁,这种编程模型叫做:异步编程模型。
    其实就是:多线程并发(效率较高。)

异步就是并发。

同步编程模型:

  • 线程t1和线程t2,在线程t1执行的时候,必须等待t2线程执行
    结束,或者说在t2线程执行的时候,必须等待t1线程执行结束,
    两个线程之间发生了等待关系,这就是同步编程模型。
    效率较低。线程排队执行。

同步就是排队。

Java中有三大变量?【重要的内容。】

  • 实例变量:在堆中。

  • 静态变量:在方法区。

  • 局部变量:在栈中。

以上三大变量中:

  • 局部变量永远都不会存在线程安全问题。
    因为局部变量不共享。(一个线程一个栈。)=
    局部变量在栈中。所以局部变量永远都不会共享

  • 实例变量在堆中,堆只有1个

  • 静态变量在方法区中,方法区只有1个

  • 堆和方法区都是多线程共享的,所以可能存在线程安全问题

局部变量+常量:不会有线程安全问题
成员变量:可能会有线程安全问题。

如果使用局部变量的话:
建议使用:StringBuilder。
因为局部变量不存在线程安全问题。选择StringBuilder。
StringBuffer效率比较低。

ArrayList是非线程安全的。
Vector是线程安全的。
HashMap HashSet是非线程安全的。
Hashtable是线程安全的。

synchronized有三种写法:

第一种:同步代码块

灵活
	synchronized(线程共享对象){
		同步代码块;
	}

synchronized后面小括号中传的这个“数据”是相当关键的。
这个数据必须是多线程共享的数据。才能达到多线程排队。

  • ()中写什么?
    那要看你想让哪些线程同步。
    假设t1、t2、t3、t4、t5,有5个线程,
    你只希望t1 t2 t3排队,t4 t5不需要排队。怎么办?
    你一定要在()中写一个t1 t2 t3共享的对象。而这个
    对象对于t4 t5来说不是共享的。

  • //Object obj2 = new Object();
    //synchronized (this){
    //synchronized (obj) {
    //synchronized ("abc") { // "abc"在字符串常量池当中。
    //synchronized (null) { // 报错:空指针。
    //synchronized (obj2) { // 这样编写就不安全了。因为obj2不是共享对象。
    //synchronized(this){
    

第二种:在实例方法上使用synchronized

  • 表示共享对象一定是this
    并且同步代码块是整个方法体。

第三种:在静态方法上使用synchronized
表示找类锁。
类锁永远只有1把
就算创建了100个对象,那类锁也只有一把

对象锁:1个对象1把锁,100个对象100把锁。
类锁:100个对象,也可能只是1把类锁

开发中应该怎么解决线程安全问题?

是一上来就选择线程同步吗?synchronized
不是,synchronized会让程序的执行效率降低,用户体验不好。
系统的用户吞吐量降低。用户体验差。在不得已的情况下再选择
线程同步机制。

  • 第一种方案:尽量使用局部变量代替“实例变量和静态变量”。

  • 第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样
    实例变量的内存就不共享了。(一个线程对应1个对象,100个线程对应100个对象,
    对象不共享,就没有数据安全问题了。)

  • 第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候
    就只能选择synchronized了。线程同步机制。

示例代码01:

/*
银行账户
    不使用线程同步机制,多线程对同一个账户进行取款,出现线程安全问题。
 */
public class Account implements Serializable{
    private static final long serialVersionUID = 1655286831056942086L;
    //账户
    private String actno;
    //余额
    private double balance;

    public Account() {
    }

    public Account(String actno, int balance) {
        this.actno = actno;
        this.balance = balance;
    }

    public String getActno() {
        return actno;
    }

    public void setActno(String actno) {
        this.actno = actno;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    //取款方法
    public void withdraw(double money){

        //取款之前的余额
        // t1和t2并发这个方法。。。。(t1和t2是两个栈。两个栈操作堆中同一个对象。)
       double before = this.getBalance();
        //取款之后的余额
        double after = before - money;

        // 在这里模拟一下网络延迟,100%会出现问题
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //更新余额
        // 思考:t1执行到这里了,但还没有来得及执行这行代码,t2线程进来withdraw方法了。此时一定出问题。
        this.setBalance(after);
    }
}
public class AccountThread extends Thread{

    // 两个线程必须共享同一个账户对象。
    private Account act;

    // 通过构造方法传递过来账户对象
    public AccountThread(Account act){
        this.act = act;
    }

    public Account getAct() {
        return act;
    }

    public void setAct(Account act) {
        this.act = act;
    }

    public void run(){

        //取款5000
        // run方法的执行表示取款操作。
        // 假设取款5000
        double money = 5000;

        // 取款
        // 多线程并发执行这个方法。
        act.withdraw(money);

        System.out.println(Thread.currentThread().getName() + "对" + act.getActno() + "取款" + money + "成功," + "账户余额为:" + act.getBalance());
    }
}
public class Test {
    public static void main(String[] args) {

        // 创建账户对象(只创建1个)
        Account act = new Account("act-no",10000);

        // 创建两个线程
        Thread t1 = new AccountThread(act);
        Thread t2 = new AccountThread(act);
        // 设置name
        t1.setName("t1");
        t2.setName("t2");
        // 启动线程取款
        t1.start();
        t2.start();

    }
}

运行结果:

在这里插入图片描述

示例代码02:

/*
银行账户
    不使用线程同步机制,多线程对同一个账户进行取款,出现线程安全问题。
 */
public class Account {
    //账户
    private String actno;
    //余额
    private double balance;

    //对象
    Object obj = new Object(); // 实例变量。(Account对象是多线程共享的,Account对象中的实例变量obj也是共享的。)

    public Account() {
    }

    public Account(String actno, int balance) {
        this.actno = actno;
        this.balance = balance;
    }

    public String getActno() {
        return actno;
    }

    public void setActno(String actno) {
        this.actno = actno;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    //取款方法
    public void withdraw(double money){

        // 以下这几行代码必须是线程排队的,不能并发。
        // 一个线程把这里的代码全部执行结束之后,另一个线程才能进来。
        /*
        线程同步机制的语法是:
            synchronized(){
                // 线程同步代码块。
            }
            synchronized后面小括号中传的这个“数据”是相当关键的。
            这个数据必须是多线程共享的数据。才能达到多线程排队。

            ()中写什么?
                那要看你想让哪些线程同步。
                假设t1、t2、t3、t4、t5,有5个线程,
                你只希望t1 t2 t3排队,t4 t5不需要排队。怎么办?
                你一定要在()中写一个t1 t2 t3共享的对象。而这个
                对象对于t4 t5来说不是共享的。

            这里的共享对象是:账户对象。
            账户对象是共享的,那么this就是账户对象吧!!!
            不一定是this,这里只要是多线程共享的那个对象就行。
            在java语言中,任何一个对象都有“一把锁”,其实这把锁就是标记。(只是把它叫做锁。)
            100个对象,100把锁。1个对象1把锁。

            以下代码的执行原理?
                1、假设t1和t2线程并发,开始执行以下代码的时候,肯定有一个先一个后。
                2、假设t1先执行了,遇到了synchronized,这个时候自动找“后面共享对象”的对象锁,
                找到之后,并占有这把锁,然后执行同步代码块中的程序,在程序执行过程中一直都是
                占有这把锁的。直到同步代码块代码结束,这把锁才会释放。
                3、假设t1已经占有这把锁,此时t2也遇到synchronized关键字,也会去占有后面
                共享对象的这把锁,结果这把锁被t1占有,t2只能在同步代码块外面等待t1的结束,
                直到t1把同步代码块执行结束了,t1会归还这把锁,此时t2终于等到这把锁,然后
                t2占有这把锁之后,进入同步代码块执行程序。

                这样就达到了线程排队执行。
                这里需要注意的是:这个共享对象一定要选好了。这个共享对象一定是你需要排队
                执行的这些线程对象所共享的。
         */
        //Object obj2 = new Object();
        //synchronized (this){
        //synchronized (obj) {
        //synchronized ("abc") { // "abc"在字符串常量池当中。
        //synchronized (null) { // 报错:空指针。
        //synchronized (obj2) { // 这样编写就不安全了。因为obj2不是共享对象。
        //synchronized(this){
            double before = this.getBalance();
            double after = before - money;
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            this.setBalance(after);

    }
}
public class AccountThread extends Thread{

    // 两个线程必须共享同一个账户对象。
    private Account act;

    // 通过构造方法传递过来账户对象
    public AccountThread(Account act){
        this.act = act;
    }

    public Account getAct() {
        return act;
    }

    public void setAct(Account act) {
        this.act = act;
    }

    public void run(){

        //取款5000
        // run方法的执行表示取款操作。
        // 假设取款5000
        double money = 5000;

        // 取款
        // 多线程并发执行这个方法。
        //synchronized (this){//这里的this是AccountThread对象,这个对象不共享!
        synchronized (act) {//这种方式也可以,只不过扩大了同步的范围,效率更低了
            act.withdraw(money);
        }
        System.out.println(Thread.currentThread().getName() + "对" + act.getActno() + "取款" + money + "成功," + "账户余额为:" + act.getBalance());
    }
}
public class Test {
    public static void main(String[] args) {

        // 创建账户对象(只创建1个)
        Account act = new Account("act-no",10000);

        // 创建两个线程
        Thread t1 = new AccountThread(act);
        Thread t2 = new AccountThread(act);
        // 设置name
        t1.setName("t1");
        t2.setName("t2");
        // 启动线程取款
        t1.start();
        t2.start();

    }
}

运行结果:

在这里插入图片描述

示例代码03:

/*
银行账户
    不使用线程同步机制,多线程对同一个账户进行取款,出现线程安全问题。
 */
public class Account implements Serializable{
    private static final long serialVersionUID = 1655286831056942086L;
    //账户
    private String actno;
    //余额
    private double balance;

    public Account() {
    }

    public Account(String actno, int balance) {
        this.actno = actno;
        this.balance = balance;
    }

    public String getActno() {
        return actno;
    }

    public void setActno(String actno) {
        this.actno = actno;
    }

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    //取款方法
    /*
    在实例方法上可以使用synchronized吗?可以的。
        synchronized出现在实例方法上,一定锁的是this。
        没得挑。只能是this。不能是其他的对象了。
        所以这种方式不灵活。

        另外还有一个缺点:synchronized出现在实例方法上,
        表示整个方法体都需要同步,可能会无故扩大同步的
        范围,导致程序的执行效率降低。所以这种方式不常用。

        synchronized使用在实例方法上有什么优点?
            代码写的少了。节俭了。

        如果共享的对象就是this,并且需要同步的代码块是整个方法体,
        建议使用这种方式。
     */
    public synchronized void withdraw(double money){

       double before = this.getBalance();
        double after = before - money;
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.setBalance(after);
    }
}
public class AccountThread extends Thread{

    // 两个线程必须共享同一个账户对象。
    private Account act;

    // 通过构造方法传递过来账户对象
    public AccountThread(Account act){
        this.act = act;
    }

    public Account getAct() {
        return act;
    }

    public void setAct(Account act) {
        this.act = act;
    }

    public void run(){

        //取款5000
        // run方法的执行表示取款操作。
        // 假设取款5000
        double money = 5000;

        // 取款
        // 多线程并发执行这个方法。
        act.withdraw(money);

        System.out.println(Thread.currentThread().getName() + "对" + act.getActno() + "取款" + money + "成功," + "账户余额为:" + act.getBalance());
    }
}
public class Test {
    public static void main(String[] args) {

        // 创建账户对象(只创建1个)
        Account act = new Account("act-no",10000);

        // 创建两个线程
        Thread t1 = new AccountThread(act);
        Thread t2 = new AccountThread(act);
        // 设置name
        t1.setName("t1");
        t2.setName("t2");
        // 启动线程取款
        t1.start();
        t2.start();

    }
}

运行结果:

在这里插入图片描述

synchronized面试题01:

doOther方法执行的时候需要等待doSome方法的结束吗?不需要

因为doOther方法没有sychronized关键字,所以doOther方法不需要到锁池中获取锁

示例代码04:

public class Exam01 {
    public static void main(String[] args) {
        MyClass c = new MyClass();

        MyThread t1 = new MyThread(c);
        MyThread t2 = new MyThread(c);

        t1.setName("t1");
        t2.setName("t2");
        t1.start();
        try {
            Thread.sleep(1000);//这个睡眠的作用是:为了保证t1线程先执行。
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();

    }
}

class MyThread extends Thread {
    private MyClass mc;

    public MyThread(MyClass mc) {
        this.mc = mc;
    }

    public void run() {

        if(Thread.currentThread().getName().equals("t1")){
            mc.doSome();
        }
        if(Thread.currentThread().getName().equals("t2")){
            mc.doOther();
        }
    }
}
class MyClass {

        public synchronized void doSome() {
            System.out.println("doSome beign");
            try {
                Thread.sleep(1000 * 5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("doSome over");
        }

        public void doOther() {
            System.out.println("doOther beign");
            System.out.println("doOther over");//因为doOther方法没有sychronized关键字,所以doOther方法不需要到锁池中获取锁
        }
    }

运行结果:

在这里插入图片描述

synchronized面试题02:

doOther方法执行的时候需要等待doSome方法的结束吗?需要

因为doOther方法有sychronized关键字,所以doOther方法需要排队(等待doSome方法执行玩释放锁之后)获取锁

示例代码05:

public class Exam02 {
        public static void main(String[] args) {

            MyClass1 a = new exam.MyClass1();
            MyThread1 t3 = new exam.MyThread1(a);
            MyThread1 t4 = new exam.MyThread1(a);

            t3.setName("t3");
            t4.setName("t4");
            t3.start();
            try {
                Thread.sleep(1000);//这个睡眠的作用是:为了保证t1线程先执行。
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            t4.start();

        }
    }

class MyThread1 extends Thread {
        private MyClass1 mc;

        public MyThread1(MyClass1 mc) {
            this.mc = mc;
        }

        public void run() {

            if(Thread.currentThread().getName().equals("t3")){
                mc.doSome();
            }
            if(Thread.currentThread().getName().equals("t4")){
                mc.doOther();
            }
        }
    }
class MyClass1 {
        public synchronized void doSome() {
            System.out.println("doSome beign");
            try {
                Thread.sleep(1000 * 5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("doSome over");
        }

        public synchronized void doOther() {
            System.out.println("doOther beign");
            System.out.println("doOther over");//因为doOther方法有sychronized关键字,所以doOther方法需要排队(等待doSome方法执行玩释放锁之后)获取锁
        }
    }

运行结果:

在这里插入图片描述

synchronized面试题03:

doOther方法执行的时候需要等待doSome方法的结束吗?不需要

因为MyClass对象是两个,两把锁

示例代码06:

public class Exam03 {
        public static void main(String[] args) {
            MyClass2 c1 = new MyClass2();
            MyClass2 c2 = new MyClass2();//创建两个对象就没有共享对象了,不需要排队获取锁

            MyThread2 t1 = new MyThread2(c1);
            MyThread2 t2 = new MyThread2(c2);

            t1.setName("t1");
            t2.setName("t2");
            t1.start();
            try {
                Thread.sleep(1000);//这个睡眠的作用是:为了保证t1线程先执行。
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            t2.start();

        }
    }

    class MyThread2 extends Thread {
        private MyClass2 mc;

        public MyThread2(MyClass2 mc) {
            this.mc = mc;
        }

        public void run() {

            if(Thread.currentThread().getName().equals("t1")){
                mc.doSome();
            }
            if(Thread.currentThread().getName().equals("t2")){
                mc.doOther();
            }
        }
    }
    class MyClass2 {

        public synchronized void doSome() {
            System.out.println("doSome beign");
            try {
                Thread.sleep(1000 * 5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("doSome over");
        }

        public synchronized void doOther() {
            System.out.println("doOther beign");
            System.out.println("doOther over");
        }
    }

运行结果:

在这里插入图片描述

synchronized面试题04:

doOther方法执行的时候需要等待doSome方法的结束吗?需要

因为静态方法是类锁,不管创建了几个对象,类锁只有

示例代码07:

public class Exam04 {
    public static void main(String[] args) {

        MyClass2 c = new MyClass2();

        MyThread2 t1 = new MyThread2(c);
        MyThread2 t2 = new MyThread2(c);

        t1.setName("t1");
        t2.setName("t2");
        t1.start();
        try {
            Thread.sleep(1000);//这个睡眠的作用是:为了保证t1线程先执行。
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t2.start();

    }
}

class MyThread3 extends Thread {
    private MyClass3 mc;

    public MyThread3(MyClass3 mc) {
        this.mc = mc;
    }

    public void run() {

        if(Thread.currentThread().getName().equals("t1")){
            mc.doSome();
        }
        if(Thread.currentThread().getName().equals("t2")){
            mc.doOther();
        }
    }
}
// synchronized出现在静态方法上是找类锁。
class MyClass3 {

    public synchronized static void doSome() {
        System.out.println("doSome beign");
        try {
            Thread.sleep(1000 * 5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("doSome over");
    }

    public synchronized static void doOther() {
        System.out.println("doOther beign");
        System.out.println("doOther over");
    }
}

运行结果:

在这里插入图片描述

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/87567.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!