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

相关文章
|
4天前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
|
6天前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。
|
6天前
|
NoSQL Redis
单线程传奇Redis,为何引入多线程?
Redis 4.0 引入多线程支持,主要用于后台对象删除、处理阻塞命令和网络 I/O 等操作,以提高并发性和性能。尽管如此,Redis 仍保留单线程执行模型处理客户端请求,确保高效性和简单性。多线程仅用于优化后台任务,如异步删除过期对象和分担读写操作,从而提升整体性能。
23 1
|
2月前
|
存储 消息中间件 资源调度
C++ 多线程之初识多线程
这篇文章介绍了C++多线程的基本概念,包括进程和线程的定义、并发的实现方式,以及如何在C++中创建和管理线程,包括使用`std::thread`库、线程的join和detach方法,并通过示例代码展示了如何创建和使用多线程。
60 1
|
2月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
35 3
|
2月前
|
Java 开发者
在Java多线程编程中,选择合适的线程创建方法至关重要
【10月更文挑战第20天】在Java多线程编程中,选择合适的线程创建方法至关重要。本文通过案例分析,探讨了继承Thread类和实现Runnable接口两种方法的优缺点及适用场景,帮助开发者做出明智的选择。
25 2
|
2月前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
41 2
|
2月前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
47 1
|
2月前
|
安全 Java 开发者
Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用
本文深入解析了Java多线程中的`wait()`、`notify()`和`notifyAll()`方法,探讨了它们在实现线程间通信和同步中的关键作用。通过示例代码展示了如何正确使用这些方法,并分享了最佳实践,帮助开发者避免常见陷阱,提高多线程程序的稳定性和效率。
55 1
|
2月前
|
Java
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件成立时被唤醒,从而有效解决数据一致性和同步问题。本文通过对比其他通信机制,展示了 `wait()` 和 `notify()` 的优势,并通过生产者-消费者模型的示例代码,详细说明了其使用方法和重要性。
44 1