可以看到,生产者生产完了,消费者立马消费,消费者消费完了,生产者又立马生产;这是一个轮流的过程,一直保持着生产者跟消费者之间的互相等待。
方法3:信号灯法
并发协作模型“生产者/消费者模式” —> 信号灯法
使用一个标志位,如果标志位为true,就等待,如果为false,就通知另一个线程。
代码模拟场景:
演员(生产者)表演节目,观众(消费者)观看节目。
用一个标志位控制线程什么时候等待,什么时候通知。
public class ThreadDemo2 { public static void main(String[] args) { TV tv = new TV(); new Player(tv).start(); new Watcher(tv).start(); } } // 生产者 -> 演员 class Player extends Thread { TV tv; public Player(TV tv) { this.tv = tv; } // 生产 @Override public void run() { for (int i = 0; i < 20; i++) { this.tv.play("电视剧第" + i + "段"); } } } // 消费者 -> 观众 class Watcher extends Thread { TV tv; public Watcher(TV tv) { this.tv = tv; } // 消费 @Override public void run() { for (int i = 0; i < 20; i++) { tv.watch(); } } } // 产品 -> 节目 class TV { // 演员表演,观众等待 // 观众观看,演员等待 // 表演的节目 String voice; // 标志位 boolean flag = true; // 表演 public synchronized void play(String voice) { if (!flag) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } this.voice = voice; System.out.println("演员表演了:" + voice); this.flag = !this.flag; // 通知观众观看 this.notifyAll(); } // 表演 public synchronized void watch() { if (flag) { try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("观众观看了:" + voice); this.flag = !this.flag; // 通知演员表演 this.notifyAll(); } }
演员演一段,观众看一段
方法四:while轮询的方式
import java.util.ArrayList; import java.util.List; public class MyList { private List<String> list = new ArrayList<String>(); public void add() { list.add("elements"); } public int size() { return list.size(); } } import mylist.MyList; public class ThreadA extends Thread { private MyList list; public ThreadA(MyList list) { super(); this.list = list; } @Override public void run() { try { for (int i = 0; i < 10; i++) { list.add(); System.out.println("添加了" + (i + 1) + "个元素"); Thread.sleep(1000); } } catch (InterruptedException e) { e.printStackTrace(); } } } import mylist.MyList; public class ThreadB extends Thread { private MyList list; public ThreadB(MyList list) { super(); this.list = list; } @Override public void run() { try { while (true) { if (list.size() == 5) { System.out.println("==5, 线程b准备退出了"); throw new InterruptedException(); } } } catch (InterruptedException e) { e.printStackTrace(); } } } import mylist.MyList; import extthread.ThreadA; import extthread.ThreadB; public class Test { public static void main(String[] args) { MyList service = new MyList(); ThreadA a = new ThreadA(service); a.setName("A"); a.start(); ThreadB b = new ThreadB(service); b.setName("B"); b.start(); } }
在这种方式下,线程A不断地改变条件,线程ThreadB不停地通过while语句检测这个条件(list.size()==5)是否成立 ,从而实现了线程间的通信。但是这种方式会浪费CPU资源。之所以说它浪费资源,是因为JVM调度器将CPU交给线程B执行时,它没做啥“有用”的工作,只是在不断地测试 某个条件是否成立。就类似于现实生活中,某个人一直看着手机屏幕是否有电话来了,而不是: 在干别的事情,当有电话来时,响铃通知TA电话来了。
notify()/notifyAll()/sleep()/wait()的区别
我们通过对这些方法分析,sleep()方法属于Thread类,而wait()/notify()/notifyAll()属于Object基础类,也就是说每个对象都有wait()/notify()/notifyAll()的功能。
sleep()不会释放锁,而wait()会释放锁。
sleep()必须捕获异常,而wait()/notify()/notifyAll()不需要捕获异常。
sleep()可以在任何地方使用,而wait()/notify()/notifyAll()只能在同步控制方法或者同步控制块里面使用。
介绍
notify():随机唤醒一个等待该对象同步锁的线程,进入就绪队列等待CPU的调度;这里的唤醒是由JVM确定唤醒哪个线程,而且不是按优先级决定。
notifyAll():唤醒所有的等待该对象同步锁的线程,进入就绪队列等待CPU调度;注意唤醒的是notify之前wait的线程,对于notify之后的wait线程是没有效果的。
wait():调用时需要先获得该Object的锁,调用后,会把当前的锁释放掉同时阻塞住;但可以通过调用该Object的notify()或者notifyAll()来重新获得锁。
sleep():在指定的时间内让正在执行的线程暂停执行,但不会释放锁。
同步方法和同步块,哪个是更好的选择?
同步块是更好的选择,同步块更要符合开放调用的原则,只在需要锁住的代码块锁住相应的对象,同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这意味着同步块之外的代码是异步执行的,这比同步整个方法更提升代码的效率。
请知道一条原则:同步的范围越小越好。
借着这一条,我额外提一点,虽说同步的范围越少越好,但是在 Java 虚拟机中还是存在着一种叫做锁粗化的优化方法,这种方法就是把同步范围变大。这是有用的,比方说 StringBuffer ,它是一个线程安全的类,自然最常用的 append() 方法是一个同步方法,我们写代码的时候会反复 append 字符串,这意味着要进行反复的加锁 -> 解锁,这对性能不利,因为这意味着 Java 虚拟机在这条线程上要反复地在内核态和用户态之间进行切换,因此 Java 虚拟机会将多次 调用append 方法的代码进行一个锁粗化的操作,将多
次的 append 的操作扩展到 append 方法的头尾,变成一个大的同步块,这样就减少了加锁 --> 解锁的次数,有效地提升了代码执行的效率。
什么是线程同步和线程互斥?线程同步和互斥的区别?同步有哪几种实现方式?
线程同步
同步,又称直接制约关系,是指多个线程彼此合作,通过一定的逻辑关系来共同完成一个任务,必须严格按照规定的某种先后顺序来运行。一般来说,同步关系中往往包含互斥,同时对临界区的资源会按照某种逻辑顺序进行访问,如先生产后使用
另一种说法
互斥解决了「多进程/线程」对临界区使用的问题,但是它没有解决「多进程/线程」协同工作的问题
我们都知道在多线程里,每个线程一定是顺序执行的,它们各自独立,以不可预知的速度向前推进,但有时候我们希望多个线程能密切合作,以实现一个共同的任务。
所谓同步,就是「多进程/线程间」在一些关键点上可能需要互相等待与互通消息,这种相互制约的等待与互通信息称为「进程/线程」同步。
简单说:同步是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。同步其实已经实现了互斥,所以同步是一种更为复杂的互斥
线程互斥
当有若干个线程都要使用某一共享资源(共享的数据和硬件资源)时,任何时刻最多只允许一个线程去使用,其它要使用该资源的线程必须等待,直到占用资源者释放该资源。线程互斥可以看成是一种特殊的线程同步。
简单说:互斥是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的,互斥是一种特殊的同步。
线程同步和互斥的区别
- 互斥是通过竞争对资源的独占使用,彼此之间不需要知道对方的存在,执行顺序是一个乱序。
- 同步是协调多个相互关联线程合作完成任务,彼此之间知道对方存在,执行顺序往往是有序的。
实现线程同步的方法
同步代码方法:sychronized 关键字修饰的方法同步代码块:sychronized 关键字修饰的代码块
使用特殊变量域volatile实现线程同步:volatile关键字为域变量的访问提供了一种免锁机制
使用重入锁实现线程同步:reentrantlock类是可冲入、互斥、实现了lock接口的锁他与sychronized方法具有相同的基本行为和语义
使用信号量首先我们要清楚临界资源和临界区的概念,临界资源就是同一时刻只允许一个线程(或进程)访问的资源,临界区就是访问临界资源的代码段。信号量是一种特殊的变量,用来控制对临界资源的使用,在多个进程或线程都要访问临界资源的时候,就需要控制多个进行或线程对临界资源的使用。信号量机制通过p、v操作实现。p操作:原子减1,申请资源,当信号量为0时,p操作阻塞;v操作:原子加1,释放资源。
在监视器(Monitor)内部,是如何做线程同步的?
在 java 虚拟机中,监视器和锁在Java虚拟机中是一块使用的,监视器监视一块同步代码块,每一个监视器都和一个对象引用相关联,为了确保一次只有一个线程执行同步代码块(线程同步),每个对象都关联着一把锁。一旦方法或者代码块被 synchronized 修饰,那么这个部分就放入了监视器的监视区域,线程在获取锁之前不允许执行该部分的代码,确保一次只能有一个线程执行该部分的代码,另外 java 还提供了显式监视器( Lock )和隐式监视器( synchronized )两种锁方案
servlet和Struts2的action是线程安全吗?SpringMVC 的 Controller 是线程安全的吗?
servlet是线程安全吗?
Servlet 不是线程安全的,servlet 是单实例多线程的,当多个线程同时访问同一个方法,是不能保证共享变量的线程安全性的。
Struts2的action是线程安全吗?
Struts2 的 action 是多实例多线程的,是线程安全的,每个请求过来都会 new 一个新的 action 分配给这个请求,请求完成后销毁。
SpringMVC 的 Controller 是线程安全的吗?
不是的
总结
Struts2 好处是不用考虑线程安全问题;Servlet 和 SpringMVC 需要考虑线程安全问题,但是性能可以提升不用处理太多的 gc,可以使用 ThreadLocal 来处理多线程的问题。
线程的安全问题体现和导致原因和解决方案?
线程的安全性问题体现
- 原子性:一个或者多个操作在CPU执行的过程中不被中断的特性。
- 可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到。
- 有序性:程序执行的顺序按照代码的先后顺序执行。
导致原因
- 线程切换带来的原子性问题
- 缓存导致的可见性问题
- 编译优化带来的有序性问题
解决办法
- JDK的Atomic开头的原子类,synchronized、Lock,可以解决原子性问题
- synchronized、volatile、Lock,可以解决可见性问题
- Happens-Before规则可以解决有序性问题
在 Java 程序中怎么保证多线程的运行安全?
方法一:使用安全类,比如 java.util.concurrent 下的类,使用原子类AtomicInteger
方法二:使用自动锁 synchronized。
方法三:使用手动锁 Lock。
手动锁 Java 示例代码如下:
你对线程优先级的理解是什么?
每一个线程都是有优先级的,一般来说,高优先级的线程在运行时会具有优先权,这依赖于线程调度的实现,这个实现是和操作系统相关的(OS dependent)。我们可以定义线程的优先级,但是这并不能保证高优先级的线程会在低优先级的线程前执行。理论上,优先级高的线程比优先级低的线程获得更多的CPU时间。实际上,线程获得的CPU时间通常由包括优先级在内的多个因素决定。线程优先级是一个 int 变量(从 1-10),1 代表最低优先级,10 代表最高优先级。
Java 的线程优先级调度会委托给操作系统去处理,所以与具体的操作系统优先级有关,如非特别需要,一般无需设置线程优先级。
线程类的构造方法和静态块是被哪个线程调用的?run方法呢?
这是一个非常刁钻和狡猾的问题。
请记住:线程类的构造方法、静态块是被 new 这个线程类所在的线程所调用的,而 run 方法里面的代码才是被线程自身所调用的。
如果说上面的说法让你感到困惑,那么我举个例子,假设 Thread2 中 new 了Thread1,main 函数中 new 了 Thread2,那么:
Thread2 的构造方法、静态块是 main 线程调用的,Thread2 的 run()方法是Thread2 自己调用的
Thread1 的构造方法、静态块是 Thread2 调用的,Thread1 的 run()方法是Thread1 自己调用的
什么是 dump 文件?你如何在 Java 中获取线程堆栈?
什么是线程dump?
线程dump是非常有用的诊断java应用问题的工具,每一个java虚拟机都有及时生成显示所有线程在某一点状态的线程dump能力。虽然每个java虚拟机线程dump打印输出格式上略微有一些不同,但是线程dump的信息包含线程基本信息、线程的运行状态、标识、调用栈;调用的堆栈包含完整的类名,所执行的方法,如果可能的话还有源代码的行数。
其中:
线程的一些基本信息:名称、优先级及id
线程状态:waiting on condition
线程的调用栈
线程锁住的资源:locked <0x3f63d600>
JVM中的许多问题都可以使用线程dump文件来进行诊断,其中比较典型的包括线程阻塞,CPU使用率过高,JVM Crash,堆内存不足和类装载等问题。
你如何在 Java 中获取线程堆栈(dump文件)?
在 windows环境中
在启动程序的控制台里敲: Ctrl +Break,线程的 dump会产生在标准输出中(缺省标准输出就是控制台,如果对输出进行了重定向,则要查看输出文件)
在 linux环境中
通过使用 jps 命令获取到线进程的 pid
通过使用 jstack pid 命令打印线程堆栈
jstack [option] pid --参数 1. -F 强制打印堆栈 2. -m 打印java 和 native(C++) 堆栈信息 3. -l 打印额外的信息,包括锁信息
另外提一点, Thread 类提供了一个 getStackTrace() 方法也可以用于获取线程堆栈。这是一个实例方法,因此此方法是和具体线程实例绑定的,每次获取获取到的是具体某个线程当前运行的堆栈
注意:jps 和ps的作用差不多,都是显示当前系统的java进程情况及进程id,top主要看cpu,内存使用情况及占用资源最多的进程由高到低排序,关注点在于资源占用情况
使用top查看目前正在运行的进程使用系统资源情况
当前占用cpu最高26.5%的进程为27796的java程序
使用jstack将所有线程信息导出到指定文件中
使用jstack [-l] pid > xxx.log命令将所有线程信息输入到指定文件中
当'jstack [-l] pid'没有响应,使用jstack -F [-m] [-l] pid >xxx.log命令强制导出堆栈dump
一个线程运行时发生异常会怎样?
如果异常没有被捕获该线程将会停止执行,如果该异常被捕获或抛出,则程序继续运行。
Thread.UncaughtExceptionHandler是用于处理未捕获异常造成线程突然中断情况的一个内嵌接口。当一个未捕获异常将造成线程中断的时候,JVM 会使用Thread.getUncaughtExceptionHandler()来查询线程的UncaughtExceptionHandler 并将线程和异常作为参数传递给 handler 的uncaughtException()方法进行处理。
Java 线程数过多会造成什么异常?
(1)线程的生命周期开销非常高
(2)消耗过多的CPU 资源
如果可运行的线程数量多于可用处理器的数量,那么有线程将会被闲置。大量空闲的线程会占用许多内存,给垃圾回收器带来压力,而且大量的线程在竞争CPU 资源时还将产生其他性能的开销。
(3)降低稳定性
JVM 在可创建线程的数量上存在一个限制,这个限制值将随着平台的不同而不同,并且承受着多个因素制约,包括JVM 的启动参数、Thread 构造函数中请求栈的大小,以及底层操作系统对线程的限制等。如果破坏了这些限制,那么可能抛出OutOfMemoryError 异常。
为什么代码会重排序?
在执行程序时,为了提高性能,处理器和编译器常常会对指令进行重排序,但是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条件:
编译器和处理器都必须遵守 as-if-serial 语义,as-if-serial 语义的意思指:不管编译器和处理器为了提高并行度怎么重排序,(单线程)程序的执行结果不能被改变。
存在数据依赖关系的不允许重排序
as-if-serial规则和happens-before规则是什么?两者的区别?
as-if-serial规则(语义)
as-if-serial 语义的意思指:不管编译器和处理器为了提高并行度怎么重排序,(单线程)程序的执行结果不能被改变。
Happens-Before规则
1、程序次序规则:在一个线程内,按照程序控制流顺序,书写在前面的操作先行发生于书写在后面的操作。
2、管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。
3、volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作。
4、线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。
5、线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测。
6、线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
7、对象终结原则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()垃圾回收方法的开始。
as-if-serial语义保证单线程内程序的执行结果不被改变,happens- before关系保证正确同步的多线程程序的执行结果不被改变。
as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens- before指定的顺序来执行的。
as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。
LockSupport详解
LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,当然阻塞之后肯定得有唤醒的方法。每个使用LockSupport的线程都会与一个许可关联,如果该许可可用,那这个线程调用park()将会立即返回,否则阻塞。如果许可不可用,则可以调用 unpark 使其可用(许可只有一个,不可累加)
LockSupport有常用的方法如下,主要有两类方法:park和unpark
public static void park(Object blocker); // 暂停当前线程 public static void parkNanos(Object blocker, long nanos); // 暂停当前线程,不过有超时时间的限制 public static void parkUntil(Object blocker, long deadline); // 暂停当前线程,直到某个时间 public static void park(); // 无期限暂停当前线程 public static void parkNanos(long nanos); // 暂停当前线程,不过有超时时间的限制 public static void parkUntil(long deadline); // 暂停当前线程,直到某个时间 public static void unpark(Thread thread); // 恢复当前线程 public static Object getBlocker(Thread t);
为什么叫park呢,park英文意思为停车。我们如果把Thread看成一辆车的话,park就是让车停下,unpark就是让车启动然后跑起来。
public class LockSupportDemo { public static Object u = new Object(); static ChangeObjectThread t1 = new ChangeObjectThread("t1"); static ChangeObjectThread t2 = new ChangeObjectThread("t2"); public static class ChangeObjectThread extends Thread { public ChangeObjectThread(String name) { super(name); } @Override public void run() { synchronized (u) { System.out.println("in " + getName()); LockSupport.park(); if (Thread.currentThread().isInterrupted()) { System.out.println("被中断了"); } System.out.println("继续执行"); } } } public static void main(String[] args) throws InterruptedException { t1.start(); Thread.sleep(1000L); t2.start(); Thread.sleep(3000L); t1.interrupt(); LockSupport.unpark(t2); t1.join(); t2.join(); } }
in t1 被中断了 继续执行 in t2 继续执行
这儿park和unpark其实实现了wait和notify的功能,不过还是有一些差别的。
park不需要获取某个对象的锁
因为中断的时候park不会抛出InterruptedException异常,所以需要在park之后自行判断中断状态,然后做额外的处理
我们再来看看Object blocker,这是个什么东西呢?这其实就是方便在线程dump的时候看到具体的阻塞对象的信息。
"t1" #10 prio=5 os_prio=31 tid=0x00007f95030cc800 nid=0x4e03 waiting on condition [0x00007000011c9000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:304) // `下面的这个信息` at com.wtuoblist.beyond.concurrent.demo.chapter3.LockSupportDemo$ChangeObjectThread.run(LockSupportDemo.java:23) // - locked <0x0000000795830950> (a java.lang.Object)
park和unpark的使用不会出现死锁的情况,这是因为park和unpark会对每个线程维持一
个许可(boolean值)
unpark调用时,如果当前线程还未进入park,则许可为true
park调用时,判断许可是否为true,如果是true,则继续往下执行;如果是false,则等待,直到许可为true
总结一下
park和unpark可以实现类似wait和notify的功能,但是并不和wait和notify交叉,也就是说unpark不会对wait起作用,notify也不会对park起作用。
park和unpark的使用不会出现死锁的情况
blocker的作用是在dump线程的时候看到阻塞对象的信息
hreadLocal 是什么?原理是什么?有哪些使用场景?
什么是ThreadLocal?
ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
package com.javaBase.LineDistance; /** * 〈一句话功能简述〉; * 〈功能详细描述〉 * * @author jxx * @see [相关类/方法](可选) * @since [产品/模块版本] (可选) */ public class TestThreadLocal { public static void main(String[] args) { ThreadLocal<Integer> threadLocal = new MyThreadLocal(); Thread t1 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 3; i++) { threadLocal.set(threadLocal.get() + 1); System.out.println("线程1:" + threadLocal.get()); } } }); Thread t2 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 3; i++) { threadLocal.set(threadLocal.get() + 1); System.out.println("线程2:" + threadLocal.get()); } } }); Thread t3 = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 3; i++) { threadLocal.set(threadLocal.get() + 1); System.out.println("线程3:" + threadLocal.get()); } } }); t1.start(); t2.start(); t3.start(); } private static class MyThreadLocal extends ThreadLocal<Integer> { @Override protected Integer initialValue() { return 0; } } }
线程2:1 线程1:1 线程2:2 线程3:1 线程1:2 线程3:2 线程2:3 线程3:3 线程1:3
可知个线程之间对ThreadLocal的操作互不影响。