三、线程的同步
多个线程共同操作共享数据时,会造成操作的不完整性,会破坏数据
当多个线程在操作共享数据时,一个线程只执行了一部分语句,另一个线程也参与进来。会导致共享数据的错误
代码如下:
package com.example.www.d4; /** * @ClassName WindowTest * @Description 卖票 * 例子:创建三个窗口卖票,总票数为100张 * 现在存在线程的安全问题 * @Author Jack * @Date 2021/11/21 23:33 * @Version 1.0 */ public class WindowTest { public static void main(String[] args) { Window w1 = new Window(); Window w2 = new Window(); Window w3 = new Window(); w1.setName("窗口一"); w2.setName("窗口二"); w3.setName("窗口三"); w1.start(); w2.start(); w3.start(); } } class Window extends Thread { private static int ticket = 100; // private static Object obj =new Object(); @Override public void run() { while (true) { // synchronized (Window.class){ if (ticket > 0) { try { sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(getName() + ":卖票,票号为" + ticket); ticket--; }else { break; } // } } } } 输出结果为: ... ... 窗口三:卖票,票号为13 窗口二:卖票,票号为13 窗口一:卖票,票号为13 窗口一:卖票,票号为10 窗口三:卖票,票号为10 窗口二:卖票,票号为8 窗口三:卖票,票号为7 窗口一:卖票,票号为6 窗口二:卖票,票号为5 窗口三:卖票,票号为4 窗口二:卖票,票号为4 窗口一:卖票,票号为4 窗口三:卖票,票号为1 窗口二:卖票,票号为0 窗口一:卖票,票号为-1
解决方法:
当一个线程操作共享数据时,等它操作完了其他线程才能继续操作
1.Synchronized的使用方法
Java对这个问题提供了专业的解决方法:同步机制
理解同步和异步
同步和异步关注的是消息通信机制
同步,就是调用某个东西是,调用方得等待这个调用返回结果才能继续往后执行。异步,和同步相反 调用方不会等待结果,而是在调用发出后调用者可用继续执行后续操作,哪个调用先执行完先处理哪个
同步异步 , 举个例子来说,一家餐厅来了5个客人,同步的意思就是说,来第一个点菜,点了个鱼,好, 厨师去捉鱼杀鱼,过了半小时鱼好了给第一位客人,开始下位一位客人,就这样一个一个来,按顺序来
相同, 异步呢,异步的意思就是来第一位客人,点什么,点鱼,给它一个牌子,让他去一边等吧,下一位客人接着点菜,点完接着点让厨师做去吧,哪个的菜先好就先端出来,
同步的优点是:同步是按照顺序一个一个来,不会乱掉,更不会出现上面代码没有执行完就执行下面的代码, 缺点:是解析的速度没有异步的快;
异步的优点是:异步是接取一个任务,直接给后台,在接下一个任务,一直一直这样,谁的先读取完先执行谁的, 缺点:没有顺序 ,谁先读取完先执行谁的 ,会出现上面的代码还没出来下面的就已经出来了,会报错;
1.同步代码块:
synchronized (对象){ //需要被同步的代码; 2.synchronized还可 以放在方法声明中,表示整个方法为同步方法。 例如: public synchronized void show (String name){ } 实现如下 package com.example.www.d4; /** * @ClassName WindowTest * @Description 卖票 * 例子:创建三个窗口卖票,总票数为100张 * 现在存在线程的安全问题 * @Author Jack * @Date 2021/11/21 23:33 * @Version 1.0 */ public class WindowTest { public static void main(String[] args) { Window w1 = new Window(); Window w2 = new Window(); Window w3 = new Window(); w1.setName("窗口一"); w2.setName("窗口二"); w3.setName("窗口三"); w1.start(); w2.start(); w3.start(); } } class Window extends Thread { private static int ticket = 100; // private static Object obj =new Object(); @Override public void run() { while (true) { synchronized (Window.class){ if (ticket > 0) { try { sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(getName() + ":卖票,票号为" + ticket); ticket--; }else { break; } } } } } 输出结果如下: ......... ......... 窗口三:卖票,票号为20 窗口三:卖票,票号为19 窗口二:卖票,票号为18 窗口二:卖票,票号为17 窗口三:卖票,票号为16 窗口三:卖票,票号为15 窗口三:卖票,票号为14 窗口三:卖票,票号为13 窗口三:卖票,票号为12 窗口三:卖票,票号为11 窗口三:卖票,票号为10 窗口三:卖票,票号为9 窗口一:卖票,票号为8 窗口三:卖票,票号为7 窗口三:卖票,票号为6 窗口三:卖票,票号为5 窗口三:卖票,票号为4 窗口三:卖票,票号为3 窗口二:卖票,票号为2 窗口三:卖票,票号为1
2.同步机制中的锁
在《Thinking in Java》 中,是这么说的:对于并发工作,你需要某种方式来防止两个任务访问相同的资源(其实就是共享资源竞争)。防止这种冲突的方法就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源,使其他任务在其被解锁之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它了。
synchronized的锁是什么 ?
任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)。
同步方法的锁:静态方法(类名.class) 、非静态方法(this )
同步代码块:自己指定,很多时候也是指定为this或类名.class
注意
必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全
一个线程类中的所有静态方法共用同一把锁(类名.class) ,所有非静态方法共用同一把锁(this) ,同步代码块(指定需谨慎)
同步的范围
1、如何找问题,即代码是否存在线程安全? (非常 重要)
(1)明确哪些代码是多线程运行的代码
(2)明确多个线程是否有共享数据
(3)明确多线程运行代码中是否有多条语句操作共享数据
2、如何解决呢?
(非常重要)
对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
即所有操作共享数据的这些语句都要放在同步范围中
3、切记
范围太小:没锁住所有有安全问题的代码
范围太大:没发挥多线程的功能。
释放锁的操作
当前线程的同步方法、同步代码块执行结束。
当前线程在同步代码块、同步方法中遇到break、return终 止了该代码块、
该方法的继续执行。
当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导
致异常结束。
当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线
程暂停,并释放锁。(后面会提到wait方法)
不会释放锁的操作
线程执行同步代码块或同步方法时,程序调用Thread. sleep()、Thread.yield()方法暂停当前线程的执行
线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器)。
应尽量避免使用suspend()和resume()来控制线程
线程的死锁问题
不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
如下
package com.caq.www; import static java.lang.Thread.sleep; /** * @ClassName ThreadTest * @Description 演示线程的死锁问题 * 1.死锁的理解: * 不同的线程分别占用对方需要的同步资源不放弃, * 都在等待对方放弃自己需要的公共资源,这样就形成了线程的死锁 * 2.说明: * 1)出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续 * 2)我们使用同步时,要避免出现死锁 * @Author Jack * @Date 2021/11/23 11:13 * @Version 1.0 */ public class ThreadTest { public static void main(String[] args) { StringBuffer s1 = new StringBuffer(); StringBuffer s2 = new StringBuffer(); //通过匿名内部类继承Thread类的方式开启一个线程 new Thread(new Runnable() { @Override public void run() { synchronized(s1){ s1.append("a"); s2.append("1"); //线程拿着s1锁,等待s2锁 try { sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (s2){ s1.append("b"); s2.append("2"); System.out.println(s1); System.out.println(s2); } } } }).start(); //通过实现Runnable接口的方式开启一个新的线程 new Thread(new Runnable() { @Override public void run() { synchronized(s2){ s1.append("c"); s2.append("3"); //线程拿着s2锁,等待s1锁 try { sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (s1){ s1.append("d"); s2.append("4"); System.out.println(s1); System.out.println(s2); } } } }).start(); } }
解决方法
专门的算法、原则
尽量减少同步资源的定义
尽量避免嵌套同步
3.Lock(锁)
从JDK 5.0开始,Java提供了更强大的线程同步机制一通过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当。
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开始访问共享资源之前应先获得Lock对象。
ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释放锁。
改进后的实例
package com.caq.www; import java.util.concurrent.locks.ReentrantLock; /** * @ClassName LockTest * @Description 解决线程安全问题方式三:Lock锁 ---JDK5.0新增 * * 1.面试题:synchronized 与Lock的异同 * 同:二者都可以解决线程安全问题 * 不同:synchronized机制在执行完相应的同步代码以后,自动释放同步监视器 * lock需要手动的启动同步(lock()),同时结束同步也需要手动执行(unlock()) * * * 2.面试题:如何创建多线程? * 1.通过继承Thread类重写run方法。生成子类的对象并调用start方法 * 2.通过实现Runnable接口并把实现类作为参数传递到Thread类的构造器中,创建Thread对象调用start()启动线程 * * 3.面试题:如何解决线程安全问题?有几种方式? * 1.通过synchronized的方式{同步代码块,同步方法} * 2.通过Lock锁的方式,手动开启同步锁对线程进行同步,并手动释放同步锁结束 * * @Author Jack * @Date 2021/11/23 15:57 * @Version 1.0 */ class Window implements Runnable{ private int ticket = 100; //实例化ReentrantLock private ReentrantLock lock = new ReentrantLock(true);//为true的话是公平的先进先出,默认是false抢占形 @Override public void run() { while (true){ try { //2.调用锁定方法lock lock.lock(); if (ticket > 0){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+":售票,票号为:"+ticket); ticket--; }else { break; } }finally { //3.调用解锁方法 lock.unlock(); } } } } public class LockTest { public static void main(String[] args) { Window w = new Window(); Thread t1 = new Thread(w); Thread t2 = new Thread(w); Thread t3 = new Thread(w); t1.setName("窗口一"); t2.setName("窗口二"); t3.setName("窗口三"); t1.start(); t2.start(); t3.start(); } }
注意:如果同步代码有异常,要将unlock()写 入finally语句块
synchronized与Lock的对比
Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域自动释放
Lock只有代码块锁,synchronized有代码块锁和方法锁
使用Lock锁, JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
优先使用顺序:
Lock -> 同步代码块(已经进入了方法体,分配了相应资源) -> 同步方法(在方法体之外)