Java多线程详解(线程同步)

您所在的位置:网站首页 多线程加锁有什么意义 Java多线程详解(线程同步)

Java多线程详解(线程同步)

2024-01-16 22:26| 来源: 网络整理| 查看: 265

        嗨喽~小伙伴们我来了,

        上一章,我们通过几个例子(点击跳转)介绍了线程安全问题,而说到线程安全就不得不提到线程同步,它是解决线程安全的一种重要方法。本章就来简单地介绍一下线程同步。

        从上一章的学习我们知道,当多个线程操作一个资源的时候,有可能由于线程的不确定切换出现数据不一致的安全问题,因此,我们要解决这个问题,就得想办法使得资源在某个时间戳只能被一个线程访问。基于这样的思想,我们提出了“队列与锁”的策略:

        通俗理解,就是将所有线程排成一个队列,给要共享的资源上把锁,只有线程获得该资源的锁之后,才能访问该资源。

        这也是线程同步的基本思想。所谓线程同步,是指同一进程中的多个线程相互协调工作从而达到一致性。使用线程同步,在解决线程安全问题的同时还能提高程序的性能。

        基于“队列与锁”的思想,JDK中封装了一些用于实现线程同步的类和关键字。我们主要介绍synchronized 和 lock 两种。

一. synchronized

        我们知道,在java中,每个对象都有一把唯一的锁,这也是synchronized实现线程同步的基础。总的来说,synchronized实现线程同步主要有三种形式:

形式特点实例同步方法锁的是当前实例对象,执行同步代码前必须获得当前实例的锁静态同步方法锁的是当前类的class对象,执行同步代码前必须获得当前类对象的锁同步代码块锁的是括号里的对象,对给定对象加锁,执行同步代码块必须获得给定对象的锁

        事实上,当两个线程同时对一个对象的某个方法进行调用时,只有一个线程能够抢到该对象的锁,因为一个对象只有一把锁。抢到该对象的锁之后,其他线程就无法访问该对象的所有synchronized方法,但仍可以访问该对象中的非synchronized方法。

        下面,我们用代码来演示synchronized的三种用法。为了突出synchronized在线程安全方面的作用,我们采用对比(有synchronized和无synchronized)的方式来介绍。

        第一种,修饰实例同步方法。首先,来看一个简单的程序:

/** * @author sixibiheye * @date 2021/8/31 * @apiNote synchronized用法举例 */ public class Synchronized implements Runnable{ //公共资源 static int count = 0; public void increase(){ count++; } @Override public void run() { for (int i = 0; i < 10000; i++) { increase(); } } public static void main(String[] args) throws InterruptedException { new Thread(new Synchronized(),"A").start(); new Thread(new Synchronized(),"B").start(); Thread.sleep(2000); System.out.println(count); } }

        如果小伙伴们学习了上一章的内容,应该很容易看出这个程序是存在线程安全问题的,它的输出结果如下:

        如果小伙伴们不太理解的话,可以去看我的上一篇博文。 

        上一篇博文中说到,对于“count++”,JVM是这样处理的:

1. 某线程从内存中读取count值到寄存器

2. 某线程在寄存器中修改count的值

3. 某线程将修改后的count值写入内存

        简单解释一下,我们开启了两个线程去执行increase()方法,如果没有任何保护机制,假设,当count的值累加到1000时,A线程从主内存中读取到寄存器的count值为1000,执行完“count++”操作后,寄存器中的count值为1001,刚想写入内存,B线程正好抢到了CPU的使用权,开始执行run()方法,由于未写入内存,B线程从内存中读取到的count值为仍为1000,执行完“count++”操作后,B线程成功地将1001写入内存,接着A线程将自己寄存器中的count值1001写入内存(覆盖了B线程的1001),由此导致,虽然执行了两个线程,但count的值只累加了一次,这样的情况多发生几次,计算结果自然就低于20000了。

        为了避免以上情况发生,我们给increase()方法加上修饰符synchronized,使得两个线程无法同时调用increase()方法,以保证上面的三步中的任何一步都不会被另外一个线程打断。

         这样,“count++”操作就永远不会因线程切换而出错。代码如下:

/** * @author sixibiheye * @date 2021/8/31 * @apiNote synchronized用法举例 */ public class Synchronized implements Runnable{ //公共资源 static int count = 0; //synchronized实现线程同步 public synchronized void increase(){ count++; } @Override public void run() { for (int i = 0; i < 10000; i++) { increase(); } } public static void main(String[] args) throws InterruptedException { new Thread(new Synchronized(),"A").start(); new Thread(new Synchronized(),"B").start(); Thread.sleep(2000); System.out.println(count); } }

        现在来看运行结果:

        没有任何问题。

        此外,使用synchronized时,有几个需要注意的地方,请看下面的代码:

/** * @author sixibiheye * @date 2021/8/31 * @apiNote synchronized用法举例 */ public class Synchronized implements Runnable{ public synchronized void running() throws InterruptedException { System.out.println("1"); Thread.sleep(1000); System.out.println("2"); } @Override public void run() { try { running(); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) throws InterruptedException { //用同一个类创建两个对象 Synchronized sync1 = new Synchronized(); new Thread(sync1).start(); Synchronized sync2 = new Synchronized(); new Thread(sync2).start(); } }

        如果我们使用不同的对象访问,那么结果有可能是不同步的:

        因为synchronized修饰实例方法时锁的对象是this对象,而使用两个对象去访问,不是同一把锁。如果我们用同一对象访问:

//只创建一个对象 Synchronized sync = new Synchronized(); new Thread(sync).start(); new Thread(sync).start();

        那结果是同步的:

          接着,我们来看synchronized修饰静态方法的例子:

/** * @author sixibiheye * @date 2021/8/31 * @apiNote synchronized用法举例 */ public class Synchronized implements Runnable{ public static synchronized void running() throws InterruptedException { System.out.println("1"); Thread.sleep(1000); System.out.println("2"); } @Override public void run() { try { running(); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) throws InterruptedException { //用同一个类创建两个对象 Synchronized sync1 = new Synchronized(); new Thread(sync1).start(); Thread.sleep(2000); Synchronized sync2 = new Synchronized(); new Thread(sync2).start(); } }

        虽然有sync1,sync2两个对象,但是它们是同一个类对象(Synchronized.class)产生的,而synchronized修饰静态方法时,锁的是 Synchronized.class,因此两个线程仍然是同步的:

         最后一种,synchronzied修饰同步代码块,它可以锁任何指定的对象,语法示例如下:

synchronized (this){ System.out.println("1"); Thread.sleep(1000); System.out.println("2"); }

        this指代当前实例对象,可以换为任何对象。

        那么为什么要使用同步代码块呢?

        在某些情况下,我们编写的方法体可能比较庞大,同时又有一些耗时的操作,如果对整个方法体进行同步,效率会大大降低,所以我们希望能够只同步必要的代码块,对于一些不需要同步的或者耗时较长的操作,放到同步代码块之外,比如:

/** * @author sixibiheye * @date 2021/8/31 * @apiNote synchronized用法举例 */ public class Synchronized implements Runnable{ public void running() throws InterruptedException { for (int i = 0; i < 5; i++) { System.out.println("这是耗时操作。"); } //需要同步的代码块写下面 synchronized (this){ System.out.println("1"); Thread.sleep(1000); System.out.println("2"); } } @Override public void run() { try { running(); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) throws InterruptedException { Synchronized sync = new Synchronized(); new Thread(sync).start(); new Thread(sync).start(); } }

        运行结果如下:

        再运行一次:

        结果表明,需要同步的代码块确实实现了同步。 

二.  lock         

        前面使用的synchronized关键字可以实现多线程间的同步互斥,其实,在JDK1.5后新增的ReentrantLock 类同样可以实现这个功能,而且在使用上比 synchronized ​​​​​​​更为灵活。

        翻阅源码,可以看到 ReentrantLock 类实现了Lock接口:

         下面我们用ReentrantLock类来实现简单的线程同步:

import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * @author sixibiheye * @date 2021/8/31 * @apiNote ReentrantLock实现线程同步 */ public class LockDemo implements Runnable{ private Lock lock = new ReentrantLock(); /** * * 按照规范, * lock()后面应紧跟try代码块, * 并将unlock()放到finally块的第一行 * */ @Override public void run() { //上锁 lock.lock(); try { for (int i = 0; i < 5; i++) { System.out.println(Thread.currentThread().getName() + "线程中的i=" + i); } } catch (Exception e) { e.printStackTrace(); } finally { //释放锁 lock.unlock(); } } public static void main(String[] args) new Thread(new LockDemo(),"A").start(); new Thread(new LockDemo(),"B").start(); } }

        其实关于Lock 和 ReentrantLock,还有许多其他的用法,限于篇幅,就不一一介绍了,有兴趣的小伙伴们可以查阅相关资料。

        了解了synchronized和ReentrantLock,对于上一章提出的三个线程安全问题,便可以轻松地解决了。下面提供使用synchronized的解决方式:

/** * @author sixibiheye * @date 2021/8/28 * @apiNote 线程安全问题一-------取钱问题 */ public class UnsafeBank { public static void main(String[] args) { //账户 Account account = new Account(100,"买房基金"); //你和你的妻子都要取钱 Drawing you = new Drawing(account,50,"你"); Drawing girlFriend = new Drawing(account,100,"妻子"); you.start(); girlFriend.start(); } } //账户 class Account{ int money; //余额 String name; //卡名 public Account(int money,String name){ this.money = money; this.name = name; } } //银行 class Drawing extends Thread{ Account account; //账户 int drawingMoney; //取的钱 int nowMoney; //现手上有的钱 public Drawing(Account account,int drawingMoney,String name){ super(name); this.account = account; this.drawingMoney = drawingMoney; } @Override public void run() { //注意锁的是account对象,而不是this synchronized (account){ if(account.money - drawingMoney < 0){ System.out.println(Thread.currentThread().getName() + "钱不够了,取不了!"); return; } try { sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } //卡内余额 account.money = account.money - drawingMoney; //手里的钱 nowMoney = nowMoney + drawingMoney; //打印 System.out.println(account.name + "余额为:" + account.money); System.out.println(Thread.currentThread().getName() + "手里的钱:" + nowMoney); } } } /** * @author sixibiheye * @date 2021/8/27 * @apiNote 线程安全问题一-------买票问题 */ public class UnsafeBuyTicket { public static void main(String[] args) { BuyTicket buyTicket = new BuyTicket(); new Thread(buyTicket,"小红").start(); new Thread(buyTicket,"小白").start(); new Thread(buyTicket,"小黑").start(); } } class BuyTicket implements Runnable{ //票数 private int tickets = 10; //线程停止的标志位 private boolean flag = true; //直接同步实例方法即可 private synchronized void buy() throws InterruptedException { //判断是否有票 if(tickets { //同步需要修改的list对象 synchronized (list){ list.add(Thread.currentThread().getName()); } }).start(); } //sleep保证上述for循环跑完再输出 Thread.sleep(3000); //输出列表大小 System.out.println(list.size()); } }

        小伙伴们可以思考一下如果使用ReentrantLock应该如何解决。

        下一章,我们将着重介绍Thread类中的一些常用方法(点击跳转),好啦~本章的内容到这就结束了,喜欢的小伙伴们点个赞鼓励支持一下吧~ 

         



【本文地址】


今日新闻


推荐新闻


    CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3