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

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

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

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

以后在开发中,我们的项目都是运行在服务器当中,

而服务器已经将线程的定义,线程对象的创建,线程

的启动等,都已经实现完了。这些代码我们都不需要

编写。


最重要的是:你要知道,你编写的程序需要放到一个

多线程的环境下运行,你更需要关注的是这些数据

在多线程并发的环境下是否是安全的(重点:*****)


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

三个条件:


条件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();
    }
}


运行结果:


0a2653c851af460fa595bd959398a8f1.png


示例代码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();
    }
}


运行结果:


2d65d23f6d4748949b924e4057485923.png


示例代码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();
    }
}


运行结果:


6de278e6d6694ce5bb08e7e842b7e74b.png


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方法不需要到锁池中获取锁
        }
    }


运行结果:


8ec4f2997fb246878c34ecd6d122b7c6.png


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方法执行玩释放锁之后)获取锁
        }
    }


运行结果:


12c3b7f3f8814309a195c64f051d4445.png


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");
        }
    }


运行结果:


34e8d716411043c08c7ffba9fbba23de.png


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");
    }
}


运行结果:


92ba0822ed0b46e1ae72df8a17d3a45b.png

相关文章
|
25天前
|
安全 Java
java 中 i++ 到底是否线程安全?
本文通过实例探讨了 `i++` 在多线程环境下的线程安全性问题。首先,使用 100 个线程分别执行 10000 次 `i++` 操作,发现最终结果小于预期的 1000000,证明 `i++` 是线程不安全的。接着,介绍了两种解决方法:使用 `synchronized` 关键字加锁和使用 `AtomicInteger` 类。其中,`AtomicInteger` 通过 `CAS` 操作实现了高效的线程安全。最后,通过分析字节码和源码,解释了 `i++` 为何线程不安全以及 `AtomicInteger` 如何保证线程安全。
java 中 i++ 到底是否线程安全?
|
20天前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
44 4
|
1月前
|
存储 安全 Java
Java-如何保证线程安全?
【10月更文挑战第10天】
|
1月前
|
算法 Java 程序员
Java中的Synchronized,你了解多少?
Java中的Synchronized,你了解多少?
|
1月前
|
Java
让星星⭐月亮告诉你,Java synchronized(*.class) synchronized 方法 synchronized(this)分析
本文通过Java代码示例,介绍了`synchronized`关键字在类和实例方法上的使用。总结了三种情况:1) 类级别的锁,多个实例对象在同一时刻只能有一个获取锁;2) 实例方法级别的锁,多个实例对象可以同时执行;3) 同一实例对象的多个线程,同一时刻只能有一个线程执行同步方法。
19 1
|
1月前
|
Java 开发者
在 Java 多线程编程中,Lock 接口正逐渐取代传统的 `synchronized` 关键字,成为高手们的首选
【10月更文挑战第6天】在 Java 多线程编程中,Lock 接口正逐渐取代传统的 `synchronized` 关键字,成为高手们的首选。相比 `synchronized`,Lock 提供了更灵活强大的线程同步机制,包括可中断等待、超时等待、重入锁及读写锁等高级特性,极大提升了多线程应用的性能和可靠性。通过示例对比,可以看出 Lock 接口通过 `lock()` 和 `unlock()` 明确管理锁的获取和释放,避免死锁风险,并支持公平锁选择和条件变量,使其在高并发场景下更具优势。掌握 Lock 接口将助力开发者构建更高效、可靠的多线程应用。
25 2
|
1月前
|
安全 Java 开发者
java的synchronized有几种加锁方式
Java的 `synchronized`通过上述三种加锁方式,为开发者提供了从粗粒度到细粒度的并发控制能力,满足了不同场景下的线程安全需求。合理选择加锁方式对于提升程序的并发性能和正确性至关重要,开发者应根据实际应用场景的特性和性能要求来决定使用哪种加锁策略。
22 0
|
2月前
|
安全 Java 调度
Java 并发编程中的线程安全和性能优化
本文将深入探讨Java并发编程中的关键概念,包括线程安全、同步机制以及性能优化。我们将从基础入手,逐步解析高级技术,并通过实例展示如何在实际开发中应用这些知识。阅读完本文后,读者将对如何在多线程环境中编写高效且安全的Java代码有一个全面的了解。
|
2月前
|
存储 安全 Java
Java并发编程之深入理解Synchronized关键字
在Java的并发编程领域,synchronized关键字扮演着守护者的角色。它确保了多个线程访问共享资源时的同步性和安全性。本文将通过浅显易懂的语言和实例,带你一步步了解synchronized的神秘面纱,从基本使用到底层原理,再到它的优化技巧,让你在编写高效安全的多线程代码时更加得心应手。
|
6月前
|
存储 安全 Java
深入理解Java并发编程:线程安全与锁机制
【5月更文挑战第31天】在Java并发编程中,线程安全和锁机制是两个核心概念。本文将深入探讨这两个概念,包括它们的定义、实现方式以及在实际开发中的应用。通过对线程安全和锁机制的深入理解,可以帮助我们更好地解决并发编程中的问题,提高程序的性能和稳定性。
下一篇
无影云桌面