未来可期 。一位正行走在编程世界中的小白 ,希望能遇到更多正在努力中的小伙伴。
线程同步机制 一、背景 我以生活中的例子来打开这个问题,例如:我们做火车买票为例子。 创建个窗口,总票数为 100 张,使用实现 Runable 接口的方式
代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class Window1 implements Runnable { private int ticket = 100 ; @Override public void run () { while (true ){ if (ticket > 0 ){ System.out.println(Thread.currentThread().getName()+":卖票,票号为:" +ticket); ticket--; }else { break ; } } } } public class WindowTest1 { public static void main (String[] args) { Window1 w1 = new Window1(); Thread t1 = new Thread(w1); Thread t2 = new Thread(w1); Thread t3 = new Thread(w1); t1.setName("窗口一" ); t2.setName("窗口二" ); t3.setName("窗口三" ); t1.start(); t2.start(); t3.start(); } }
运行结果
在卖票的过程中,通过上面的代码运行的代码,在最后出现了重票、错票,从而出现了线程的安全问题。 出现线程安全的原因,就是当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,一起来操作车票。 出现了问题,总要有解决的办法,在 java 多线程就给我们提供了这个解决办法方法。 我先简单的描述一下解决问题的方法,当一个线程 a 在操作 ticket 的时候,其他线程不能参与进来。直到线程 a 操作完 ticket 时,其他线程才可以开始操作 ticket.这种情况即使线程 a 出现了阻塞,也不能被改变。 二、解决方法 方式一:同步代码块 1 2 3 4 语法: synchronized(同步监视器){ }
说明:
操作共享数据的代码,即为需要被同步的代码 –> 不能包含代码多了,也不能包含的代码少了。 共享数据,多个数据共同操作的变量,比如:ticket 就是共享数据。 同步监视器俗称 锁 。任何一个类的对象,都可以充当锁。 要求:多个线程必须要用同一把锁。 ① 解决继承 Thread 类线程安全问题 代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 class Window2 extends Thread { private static int ticket = 100 ; @Override public void run () { while (true ){ synchronized(Window2.class){ if (ticket > 0 ){ try { Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(getName()+":卖票,票号为:" +ticket); ticket--; }else { break ; } } } } } public class Window2Test { public static void main (String[] args) { Window2 t1 = new Window2(); Window2 t2 = new Window2(); Window2 t3 = new Window2(); t1.setName("窗口一" ); t2.setName("窗口二" ); t3.setName("窗口三" ); t1.start(); t2.start(); t3.start(); } }
运行结果:
② 解决实现 Runable 接口线程安全 代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 class Window1 implements Runnable { private int ticket = 100 ; @Override public void run () { while (true ){ synchronized(this){ if (ticket > 0 ){ System.out.println(Thread.currentThread().getName()+":卖票,票号为:" +ticket); ticket--; }else { break ; } } } } } public class WindowTest1 { public static void main (String[] args) { Window1 w1 = new Window1(); Thread t1 = new Thread(w1); Thread t2 = new Thread(w1); Thread t3 = new Thread(w1); t1.setName("窗口一" ); t2.setName("窗口二" ); t3.setName("窗口三" ); t1.start(); t2.start(); t3.start(); } }
运行结果:补充:
在实现 Runable 接口创建多线程的方式中,我们可以考虑使用this 充当同步监视器。 在继承 Thread 类创建多线程的方式中,慎用 this 充当同步监视器,考虑使用当前类充当同步监视器。
方式二:同步方法 如果操作共享数据的代码完整的声明在一个方法中,我们不妨碍将此方法声明同步的
① 使用同步方法解决 Thread 类的线程安全问题 代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 class Window3 extends Thread { private static int ticket = 100 ; @Override public void run () { while (true ){ show(); } } private static synchronized void show () { if (ticket > 0 ){ try { Thread.sleep(100 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+":卖票,票号为:" +ticket); ticket--; } } } public class WindowTest3 { public static void main (String[] args) { Window3 w1 = new Window3(); Window3 w2 = new Window3(); Window3 w3 = new Window3(); w1.setName("窗口一" ); w2.setName("窗口二" ); w3.setName("窗口三" ); w1.start(); w2.start(); w3.start(); } }
运行结果:
② 使用同步方法解决实现 Runable 接口线程安全 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 class Window4 implements Runnable { private int ticket = 100 ; @Override public void run () { while (true ){ show(); } } private synchronized void show () { if (ticket > 0 ){ System.out.println(Thread.currentThread().getName()+":卖票,票号为:" +ticket); ticket--; } } } public class WindowTest4 { public static void main (String[] args) { Window4 w = new Window4(); 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(); } }
关于同步方法的总结:
同步方法仍然涉及到同步监视器只是不需要我们显示的声明。 非静态的同步方法,同步监视器是:this 静态的同步方法,同步监视器是: 当前类本身 方式三:Lock 锁 – JDK5.0 新增 代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 class Window5 implements Runnable { ReentrantLock lock = new ReentrantLock(); private int ticket = 100 ; @Override public void run () { while (true ){ try { 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 { lock.unlock(); } } } } public class LockTest { public static void main (String[] args) { Window5 w = new Window5(); Thread t1 = new Thread(w); Thread t2 = new Thread(w); Thread t3 = new Thread(w); t1.setName("窗口1" ); t2.setName("窗口2" ); t3.setName("窗口3" ); t1.start(); t2.start(); t3.start(); } }
运行结果:
总结 使用的优先顺序
1 Lock --> 同步代码块(已经进入方法体,分配了相对应资源) --> 同步方法(在方法体之外)
利弊
同步的方式,解决了线程的安全问题。–> 好处 操作同步代码时,只能一个线程参与,其他线程等待。就相当于是一个的单线程的过程,效率低。 synchronzied 与 Lock 的异同
synchronzied 机制在执行完相对应的同步代码块后,自动的释放同步监视器 Lock 需要手动的启动(lock()),同时结束时也需要手动的实现(unlock()); 死锁 死锁的理解:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
说明:
出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续 我们使用同步时,要避免出现死锁。 代码示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 class A { public synchronized void foo (B b) { System.out.println("当前线程名: " + Thread.currentThread().getName() + " 进入了A实例的foo方法" ); System.out.println("当前线程名: " + Thread.currentThread().getName() + " 企图调用B实例的last方法" ); b.last(); } public synchronized void last () { System.out.println("进入了A类的last方法内部" ); } } class B { public synchronized void bar (A a) { System.out.println("当前线程名: " + Thread.currentThread().getName() + " 进入了B实例的bar方法" ); System.out.println("当前线程名: " + Thread.currentThread().getName() + " 企图调用A实例的last方法" ); a.last(); } public synchronized void last () { System.out.println("进入了B类的last方法内部" ); } } public class DeadLock implements Runnable { A a = new A(); B b = new B(); public void init () { Thread.currentThread().setName("主线程" ); a.foo(b); System.out.println("进入了主线程之后" ); } @Override public void run () { Thread.currentThread().setName("副线程" ); b.bar(a); System.out.println("进入了副线程之后" ); } public static void main (String[] args) { DeadLock dl = new DeadLock(); new Thread(dl).start(); dl.init(); } }
运行结果:
线程通信 一、线程通信涉及到的三个方法 wait() :一旦执行此方法,当前线程就会进入阻塞状态,并释放同步监视器。notify() :一旦执行方法,就会唤醒被 wait 的第一个线程,如果有多个线程被 wait,就唤醒优先级最高的那个。notifyAll() :一旦执行此方法,就会唤醒所有被 wait()的线程。
代码示例:
线程通信的例子:使用两个线程打印 1-100。线程 1, 线程 2 交替打印
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 class Number implements Runnable { private int number = 1 ; Object obj = new Object(); @Override public void run () { while (true ){ synchronized(obj){ obj.notify(); if (number <= 100 ){ try { Thread.sleep(10 ); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+":" +number); number++; try { obj.wait(); } catch (InterruptedException e) { e.printStackTrace(); } }else { break ; } } } } } public class CommunicationTest { public static void main (String[] args) { Number number = new Number(); Thread t1 = new Thread(number); Thread t2 = new Thread(number); t1.setName("线程1" ); t2.setName("线程2" ); t1.start(); t2.start(); } }
运行结果:
二、说明 wait(),notify().notifyAll()**:三个方法必须用在同步代码块或同步方法中。 **wait(),notify(),notifyAll() :三个方法的调用者必须是同步代码块同步方法的同步监视器。否则会出现IllegalMonitorStateException 异常wait(),notify(),notifyAll()**:三个方法时定义在 java.lang.Object**类中。
三、释放锁的操作: ① 当前线程的同步方法、同步代码执行结束。 ② 当前线程在同步代码块、同步方法中遇到 break、return 终止该代码块、该方法的继承执行。 ③ 当前线程在同步代码块、同步方法中出现未处理的 Error 或 Exception,导致异常结束。 ④ 当前线程在同步代码块、同步方法中执行了线程对象的 wait()方法,当前线程暂停,并释放锁
四、不会释放锁的操作 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield(); 方法暂停当前线程的执行 线程执行同步代码块时,其他线程调用了线程的 supend()方法将线程挂起,该线程不会释放锁(同步监视器) 五、小结 sleep()与 wait()的异同
相同点:一旦执行方法,都可以使得当前的线程进入阻塞状态。 不同点: <1>两个方法声明的位置不同:Thread 类中声明sleep()**, Object类中声明 wait()** <2> 调用的要求不同:sleep() 可以在任何需要的场景下调用。wait() 必须使用在同步代码块或同步方法中。 java 多线程基础到这里结束了,在写博客同时一边复习自己所学的知识点,给自己不断积累知识点。