基本概念
进程:一个程序在运行过程中的动态指令集和系统内存、资源的集合。操作系统调度和分配资源的基本单位
线程:线程是进程中的一条执行路径,CPU调度的最小单位。
- cpu通过时间片轮转算法能够并发执行多个任务,而cpu执行单个指令的时间非常短,就可以在宏观上实现多任务并行的效果,但是任务切换需要消耗比较多的cpu时间周期,所以开辟的线程过多容易导致卡顿和cpu资源浪费
- CPU目前都是多核的,内核数是物理层面CPU的核数,内核数与线程数是一比一的关系,但是目前很多CPU通过引入超线程技术,使得CPU能够同时运行比内核数更多的线程,即逻辑核心数(也可成为逻辑处理器),所谓的超线程技术本质上这也是通过时间片轮转实现的。
- 一个程序运行后可以包含多个进程,一个进程可以包含多个线程,但至少包括一个线程。线程之间相互独立,各自拥有独立的堆栈,程序计数器和局部变量,但不共享系统资源,如公共的变量,所以需要考虑线程之间同步的问题。
- Java中线程的执行是抢占 式的并发执行——并发是指同一时刻只有一条指令集被执行,但多个进程之间被快速轮换执行,使得宏观上具备多个进程同时进行的效果。并行是指在同一时刻,多条指令集在多个处理器上同时执行。简而言之,并行是真正意义上的同时执行不同的任务,并发是单位时间内能执行的任务数量
- 进程是操作系统调度调度和管理的基本单位,所以操作系统不会直接参与线程的调度和管理,那是进程内部的工作。而线程是程序使用CPU的最基本单位。
- finalize()方法由Finalizer线程执行,但这个方法不可靠,一个重要原因该线程是守护线程,如果用户在退出程序时候想使用finalize方法释放一些资源,很可能因为守护线程随着主线程一起退出而未执行。
tip:Java程序运行在JVM进程中,JVM是多线程的,因为至少启动了主线程和垃圾回收线程
线程启动的方法, 1、继承Thread类 2、实现Runnable接口 Callable本质上可以归为这个方法,因为Callable类会被封装到FutureTask中运行,而FutureYask是间接继承了Runnable接口的,本质上和第二种方法是殊途同归的。
线程操作方法
- setName()和getName():而直接调用后者有默认的实现。或者通过带String参数的构造方法来设置线程名,这需要重写Thread(String var1)构造函数 Thread.currentThread().getName();获取当前运行线程的名字
- suspend:暂停线程后不释放锁,容易造成死锁
- stop:线程被粗暴终结,的资源不能释放
- interrupt:通知线程可以结束,线程可以完全不理会, 结合isInterrupted使用 注意interrupted方法和isInterrupted的区别,前者会在调用后清除interrupted status,后者不会。 中断线程不建议自主设置标志位,因为如果是通过判断标志位进入线程sleep、wait、take等方法,线程被阻塞,就不会再判断标志位,而使用interrupt就不会产生这样的问题,因为他们可以自动监控线程中断状态。(如果是在runnable中想使用这个方法,就使用Thread.currentThread.isInterrupted即可) 使用interrupt中断线程
public class HasInterruptException extends Thread {
@Override
public void run() {
super.run();
while (!isInterrupted()) {
System.out.println(Thread.currentThread().getName() + "is running");
}
System.out.println(Thread.currentThread().getName() + "interrupt flag is " + isInterrupted());
}
public static void main(String[] args) {
HasInterruptException hasInterruptException = new HasInterruptException();
hasInterruptException.start();
try {
Thread.sleep(400);
} catch (InterruptedException e) {
e.printStackTrace();
}
hasInterruptException.interrupt();
}
}
线程阻塞方法也可以监控线程interrput方法,如果处于阻塞状态,会被程序检测出来,并抛出interruptedException异常,同时将interrupt状态恢复,但线程会继续运行剩余的任务。
死锁状态的线程不会理会中断
public class HasInterruptException extends Thread {
@Override
public void run() {
super.run();
while (!isInterrupted()) {
try {
sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "is running");
System.out.println(Thread.currentThread().getName() + "now interrupt state " + isInterrupted());
}
System.out.println(Thread.currentThread().getName() + "interrupt flag is " + isInterrupted());
}
public static void main(String[] args) {
HasInterruptException hasInterruptException = new HasInterruptException();
hasInterruptException.start();
try {
Thread.sleep(400);
} catch (InterruptedException e) {
e.printStackTrace();
}
hasInterruptException.interrupt();
}
}
如果需要在有阻塞代码时也能正常中断线程,需要在阻塞方法检查到中断时再添加中断方法,在interrupt()之前可以添加一些善后处理代码
@Override
public void run() {
super.run();
while (!isInterrupted()) {
try {
sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
//如果需要在有阻塞代码时也能正常中断线程,需要在阻塞方法检查到中断时再添加中断方法,在此之前可以添加一些善后处理代码
interrupt();
}
System.out.println(Thread.currentThread().getName() + "is running");
System.out.println(Thread.currentThread().getName() + "now interrupt state " + isInterrupted());
}
System.out.println(Thread.currentThread().getName() + "interrupt flag is " + isInterrupted());
}
由此可知,阻塞方法中检测到中断会抛出异常到原因是为了提示程序这时候线程不能直接中断,需要正确处理善后,再执行中断
- start:Thread仅仅是一个线程相关类,new出来后不调用start方法是没有开启线程的。 start方法只能调用一次,多次调用会抛出IllegalThreadStateException异常 如果new一个Thread之后直接调用run方法,和调用一个普通方法效果完全一致,在主线程中执行
- yield:执行该方法的线程会让出cpu的执行权,进入就绪状态。不会释放锁,调用该方法后cpu可能还会重新选中该线程执行,所以这个方法不可靠。
- join:让调用该方法的线程执行完毕之后,其他线程才能执行,必须放在start之后。有三个重载方法。
static class A extends Thread {
@Override
public void run() {
super.run();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " runing end");
}
}
public static void main(String[] args) {
A a = new A();
A aa = new A();
a.start();
aa.start();
try {
aa.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- setPriority:设置线程优先级,这个方法不完全可靠,也有一定效果
- setDaemon(boolean)守护线程,当进程中所有线程都是守护线程时候,进程结束 守护线程因为用户线程结束而被动结束时候,线程中的方法不一定能完全执行,如finally语句不一定能执行
- notify()/notifyAll() 不会释放锁
注: wait(),notify(),notifyAll()应该放在synchronized关键字所保护的内容内,wait方法调用时调用方释放锁,但是notify/notifyAll调用时要等到同步带吗块的代码执行完毕才释放锁,所以notify/notifyAll一般会被放在同步代码块的最后一行。
- wait() wait是指在一个已经进入了同步锁的线程内,让自己暂时让出同步锁,以便其他正在等待此锁的线程可以得到同步锁并运行,只有其他线程调用了notify方法(notify并不释放锁,只是告诉调用过wait方法的线程可以去参与获得锁的竞争了,但不是马上得到锁,因为锁还在别人手里,别人还没释放。如果notify方法后面的代码还有很多,需要这些代码执行完后才会释放锁,可以在notfiy方法后增加一个等待和一些代码),调用wait方法的线程就会解除wait状态并争抢cpu使用权,抢到了之后才会执行wait之后的代码。可以指定停止的时间参数,也可不指定。
- sleep() sleep就是正在执行的线程主动让出cpu,cpu去执行其他线程,在sleep指定的时间过后,cpu才会回到这个线程上继续往下执行,如果当前线程进入了同步锁,sleep方法并不会释放锁,即使当前线程使用sleep方法让出了cpu,但其他被同步锁挡住了的线程也无法得到执行。当休眠时间到并跑完同步代码之后才会释放锁。和wait一样,调用时候都会把cpu资源让出。
注:obj=null 只是表示这个引用不再指向该对象,不代表这个对象不存在了,除非所有强引用都被抹掉,才会被系统GC
PDD pp = new PDD();
PDD ppp = pp;
System.out.println(pp);
System.out.println(ppp);
pp = null;
System.out.println(pp);
System.out.println(ppp);
输出
//com.example.annotation.PDD@6d06d69c
//com.example.annotation.PDD@6d06d69c
//null
//com.example.annotation.PDD@6d06d69c
想让对象被垃圾回收机制回收,需要保证没有强引用指向这个对象
注:线程同步阻塞的一个例子
@Override
public void run() {
while (!Thread.currentThread().isInterrupted()) {
System.out.println(Thread.currentThread().getName() + " start");
synchronized (this) {
try {
System.out.println(Thread.currentThread().getName() + "sleep start");
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "sleep end");
}
System.out.println(Thread.currentThread().getName() + " continue...");
}
}
所有遇到同步代码块或者同步方法的线程都会被阻塞,除非抢到锁,抢到锁之后会继续往下执行,而不会跳过同步代码块/同步方法再执行。
线程的生命周期
注:
-
外部调用Lock方法把其他线程阻塞后,底层调用的是LockSupport.unpark和LockSupport.parkNanos/parkUtil的方法,线程真正进入的是等待状态,而不是阻塞状态
-
线程阻塞和等待的区别在于程序主动被停滞还是被动
实战演练
三个窗口售卖总共100张票
public class SellingTickets implements Runnable{
private int mTicketNum = 100;
@Override
public void run() {
while(mTicketNum>0) {
System.out.println(Thread.currentThread().getName()+" selling the ticket NO."+mTicketNum);
mTicketNum--;
}
}
}
public class SellingWindow extends Thread {
public SellingWindow() {
super();
}
public SellingWindow(Runnable runnable, String s) {
super(runnable, s);
}
}
public static void main(String[] args) {
SellingTickets sellingTask = new SellingTickets();
SellingWindow window1 = new SellingWindow(sellingTask,"window1");
SellingWindow window2 = new SellingWindow(sellingTask,"window2");
SellingWindow window3 = new SellingWindow(sellingTask,"window3");
window1.start();
window2.start();
window3.start();
}
//结果中虽然能够实现基本卖票效果,但是票的顺序和总票数有一点微小偏差
所以需要做线程同步
线程同步示例 多线程代码中不会等到一个方法运行结束后再跑另一个线程的方法,可能线程A的方法走到一半,线程B方法就开始执行。 以下是一个关于线程同步的例子:
public class TestClass {
private class Window implements Runnable {
private int mTicket = 1;
@Override
public void run() {
while (mTicket <= 100) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "正在出售第 " + (mTicket++) + " 张票");
}
}
}
public static void main(String[] args) {
Window window = new TestClass().new Window();
Thread th1 = new Thread(window, "窗口1");
Thread th2 = new Thread(window, "窗口2");
Thread th3 = new Thread(window, "窗口3");
th1.start();
th2.start();
th3.start();
}
/**
* 问题1.相同的票卖了多次,原因是CPU的操作是原子性的,mTicket在输出后才被++,然后将新值赋值给mTicket,
* 所以当不同线程抢占CPU的时候,会出现该变量卡在被赋值前,多个线程运行了输出语句。
*
* 问题2.和出现超过100的票,原因是线程切换随机性和代码延迟造成的,
* 不同线程都成功通过了mTicket <= 100的判断后引发的并发异常
*/
}
多线程同步问题有两个出现条件
- 有共享数据
- 有多条操作共享数据的代码
解决问题方式:
1、synchronized:同步方法或代码块,区分对象锁和类锁,但本质上都是对象锁
错误的加锁原因示例:锁对象地址改变了,即不是同一个锁了
public static void main(String[] args) throws InterruptedException {
Worker worker=new Worker(1);
//Thread.sleep(50);
for(int i=0;i<5;i++) {
new Thread(worker).start();
}
}
private static class Worker implements Runnable{
private Integer i;
public Worker(Integer i) {
this.i=i;
}
@Override
public void run() {
synchronized (i) {
Thread thread=Thread.currentThread();
System.out.println(thread.getName()+"--@"+System.identityHashCode(i));
i++;//底层实现是new了一个新的Ineger对象
System.out.println(thread.getName()+"-------"+i+"-@"+System.identityHashCode(i));
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(thread.getName()+"-------"+i+"--@"+System.identityHashCode(i));
}
}
}
同步代码块 Synchronized(obj){} 多个线程使用同一把锁才有效果,弊端就是当线程增加时,每次运行到同步代码块都需要判断一次同步锁,耗费系统资源。 同步锁 synchronized private void function() {},synchronized也可以放到方法修饰符后面,同步的锁是this。 静态方法的锁是对应的类的字节码对象,类名.class
使用synchronized关键字时还需要注意
- 该关键字是锁住了包含的代码,并非锁住了CPU的使用权,当同步代码块被一个线程抢占时,其他线程在同一时间不能访问同步代码块,但可以访问非同步代码块。
- synchronized会封锁住所有影响的模块,如持有同一个对象锁的几个不同方法。当一个方法中的对象锁被某个线程占用,其他持锁的方法也不能被别的线程执行。
- synchronized关键字不能继承。对于父类中的 synchronized 修饰方法,子类在覆盖该方法时,默认情况下不是同步的,必须显示的使用 synchronized 关键字修饰才行。
- 在定义接口方法时不能使用synchronized关键字。
- 构造方法不能使用synchronized关键字,但可以使用synchronized代码块来进行同步。
2、Lock方法 有几个实现类,在需要同步的代码中使用,比较常用的是reentrantLock类,方法lock(),unlock();建议采用try finally结构。
try {
mLock.lock();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(mTicketNum > 0) {
System.out.println(Thread.currentThread().getName() + " selling the ticket NO." + mTicketNum);
mTicketNum--;
}
}finally {
mLock.unlock();
}
3、volatile:只能保证并发修改但可见性,即被修饰变量的最新值被能第一时间暴露给其他线程,但不能保证并发修改但原子性。比较适合一写多读但使用场景
4、ThreadLocal: 线程本地变量,或叫做线程本地存储,可以让每个线程拥有自己都变量副本,不会和其他线程产生冲突,实现了线程数据隔离 实现原理可以用下面都代码简单演示
public class MyThreadLocal<T> {
private Map<Thread, T> threadTMap = new HashMap<>();
public synchronized T get() {
return threadTMap.get(Thread.currentThread());
}
public synchronized void set(T t) {
threadTMap.put(Thread.currentThread(), t);
}
}
ThreadLocal测试代码
static ThreadLocal<String> threadLocal = new ThreadLocal<>();
static ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
//static MyThreadLocal<String> threadLocal = new MyThreadLocal<>();
/**
* 运行3个线程
*/
public void StartThreadArray() {
Thread[] runs = new Thread[3];
for (int i = 0; i < runs.length; i++) {
runs[i] = new Thread(new TestThread(i));
}
for (int i = 0; i < runs.length; i++) {
runs[i].start();
}
}
/**
* 类说明:测试线程,线程的工作是将ThreadLocal变量的值变化,并写回,看看线程之间是否会互相影响
*/
public static class TestThread implements Runnable {
int id;
public TestThread(int id) {
this.id = id;
}
public void run() {
String threadName = Thread.currentThread().getName();
threadLocal.set("线程" + id);
if(id==1) {
threadLocal2.set(id);//线程1才会执行
}
System.out.println(threadName + ":" + threadLocal.get());
}
}
public static void main(String[] args) {
UseThreadLocal test = new UseThreadLocal();
test.StartThreadArray();
}
上述代码中ThreadLocal切换成自定义都MyThreadLocal也是完全可以的
但这只是测试代码,实际上按照MyThreadLocal的是想方案,多个线程会去争夺MyThreadLocal中的map容器,同样造成资源争夺的问题,所以真正的ThreadLocal的实现方式为:
每个线程中维护一个Entry[],该数组是一个threadLocal和value的键值对。这样多个线程就实现了真正意义上的数据隔离。
上述代码使用原生ThreadLocal最终输出为;
Thread-0:线程0 null
Thread-1:线程1 1
Thread-2:线程2 null
ThreadLocal 引发的内存泄漏问题
public static final int TASK_POOL_SIZE = 200;
final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5,5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>());
static class Localvirable {
private byte[] a = new byte[1024*1024*5];
}
final static ThreadLocal<Localvirable> threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < TASK_POOL_SIZE; i++) {
poolExecutor.execute(new Runnable() {
@Override
public void run() {
threadLocal.set(new Localvirable());
System.out.println("use local variable");
//threadLocal.remove();
}
});
Thread.sleep(500);
}
}
如果不加threadLocal.remove()方法,就会产生内存泄漏。 原因分析:
ThreadLocalMap的Entry中的value没有及时清理,而value是被当前线程强引用的,导致内存不断攀升。所以remove方法是必须的。
其实ThreadLocal已经在设计上做了针对内存泄漏的优化,首先ThreadLocalMap的Entry继承于WeakReference<ThreadLocal<?>>,已经避免ThreadLocal这个对象无法被清理而产生的内存泄漏,同时在ThreadLocal的set和get方法中会自动调用expungeStaleEntry方法清除key为null的entry,但是在快速频繁的操作ThreadLocal的过程中不一定能及时生效,如上面代码中key也就是ThreadLocal随着gc会被回收置空,但是value不能及时释放,需要等待调用expungeStaleEntry,所以内存泄漏量会缓慢地增加。所以保险起见一定要在使用完ThreadLocal之后调用一次remove方法,其实remove方法的实现是把key置空并且调用expungeStaleEntry方法
ThreadLocal引发的线程不安全问题
static class Number {
private int value;
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
private static Number number = new Number();
private static ThreadLocal<Number> value = new ThreadLocal<>();
private static class Task implements Runnable {
@Override
public void run() {
number.setValue(number.getValue() + 1);
value.set(number);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " value = " + ((Number) value.get()).value);
}
}
public static void main(String[] args) {
Task task = new Task();
for (int i = 0; i < 5; i++) {
new Thread(task).start();
}
}
理想状态下输出应该是value值依次从0增加到5,但实际上大部分情况输出是如下:
Thread-1 value = 5
Thread-3 value = 5
Thread-0 value = 5
Thread-2 value = 5
Thread-4 value = 5
原因就是每个线程的ThreadLocalMap虽然保留了独立的Number的引用,但是该引用指向了同一个实例,导致每次对number +1操作就影响到其他线程,而因为数据跨线程可见性的问题,value有时候同时为3或者4等其他值。
线程安全的工具类
线程安全是指多个Thread访问同一个对象的相同方法时不会出现同步的问题,比如多个线程不会同时调用一个集合的add()方法。但如果我们要使用线程非原子方法的组合,就不是线程安全了,比如
public void method() {
boolean absent = !list.contains(x);
if(absent)
list.add(x);
}
StringBuffer, Vector, Hashtable,通过源码可以看到他们的方法都加锁了。 Vector目前不推荐使用了,但可以通过Collections.synchronizedList()将线程不安全的集合转变为线程安全的集合。如
List<String> list = Collections.synchronizedList(new ArrayList<String>());
其内部实现也是将基本操作加上synchronized关键字,类似于Vector,所以也不推荐使用 说说线程安全包装:Collections.synchronizedList
线程同步的弊端
同步的弊端,一是效率低,因为每次运行到锁代码都要判断锁;二是可能产生死锁问题。 死锁:两个或以上的线程在争夺资源的时候,产生的相互等待的现象
//举个例子
class DeadLockTask implements Runnable {
public static Object myLock1 = new Object();
public static Object myLock2 = new Object();
private boolean mFlag;
public DeadLockTask(boolean mFlag) {
this.mFlag = mFlag;
}
@Override
public void run() {
if (mFlag) {
synchronized (myLock1) {
System.out.println("if MyLock1");
synchronized (myLock2) {
System.out.println("if MyLock2");
}
}
} else {
synchronized (myLock2) {
System.out.println("else MyLock2");
synchronized (myLock1) {
System.out.println("else MyLock1");
}
}
}
}
}
public class TestClass {
public static void main(String[] args) {
DeadLockTask task1 = new DeadLockTask(true);
DeadLockTask task2 = new DeadLockTask(false);
Thread th1 = new Thread(task1);
Thread th2 = new Thread(task2);
th1.start();
th2.start();
}
}
输出的是
else MyLock2
if MyLock1
解决死锁的思路:
- 明确抢锁的顺序
- 抢不到锁就释放持有的锁 下面这个例子是抢不到锁就释放的例子:
private static Lock No13 = new ReentrantLock();//第一个锁
private static Lock No14 = new ReentrantLock();//第二个锁
private static final Random r = new Random();
public static void HeriaDo() throws InterruptedException {
String threadName = Thread.currentThread().getName();
while (true) {
if (No13.tryLock()) {
try {
if (No14.tryLock()) {
try {
System.out.println(threadName + "do sth");
break;
} finally {
No14.unlock();
}
}
} finally {
No13.unlock();
}
}
//Thread.sleep(r.nextInt(3));
}
}
public static void LeoDo() throws InterruptedException {
String threadName = Thread.currentThread().getName();
while (true) {
if (No14.tryLock()) {
try {
if (No13.tryLock()) {
try {
System.out.println(threadName + "do sth");
break;
} finally {
No13.unlock();
}
}
} finally {
No14.unlock();
}
}
//Thread.sleep(r.nextInt(3));
}
}
public static void main(String[] args) throws InterruptedException {
new Thread() {
@Override
public void run() {
super.run();
try {
HeriaDo();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}.start();
LeoDo();
}
Thread.sleep(r.nextInt(3))是为了防止活锁,线程一直在争抢资源,但都没能抢到两把锁,执行业务代码
注:补充一个概念叫线程饥饿,即线程优先级比较低或者人品差一直抢不到执行权
线程间的通信
指的是不同线程间针对同一资源进行操作 不同线程之间都要加锁,且加的锁必须是一样的,原理参照上述synchronize关键字注意点。 经典例子生产者消费者实例代码
class SetTask implements Runnable {
private boolean mFlag;
private Student mStudent;
public SetTask(Student student) {
this.mStudent = student;
}
@Override
public void run() {
while (true) {
synchronized (mStudent) {
if (mFlag) {
mStudent.name = "Leo";
mStudent.age = 24;
} else {
mStudent.name = "Franz";
mStudent.age = 25;
}
mFlag = !mFlag;
}
}
}
}
class GetTask implements Runnable {
private Student mStudent;
public GetTask(Student mStudent) {
this.mStudent = mStudent;
}
@Override
public void run() {
while (true) {
synchronized(mStudent) {
System.out.println(mStudent.name + " " + mStudent.age);
}
}
}
}
class Student {
String name;
int age;
}
public class TestClass {
public static void main(String[] args) {
Student student = new Student();
SetTask task1 = new SetTask(student);
GetTask task2 = new GetTask(student);
Thread th1 = new Thread(task1);
Thread th2 = new Thread(task2);
th1.start();
th2.start();
}
上述代码解决了线程安全问题,但没有处理好线程之间有序地通信(同一个线程仍然可能连续执行很多次),这就需要等待唤醒机制。
class SetTask implements Runnable {
private boolean mFlag;
private Student mStudent;
public SetTask(Student student) {
this.mStudent = student;
}
@Override
public void run() {
while (true) {
synchronized (mStudent) {
if(mStudent.flag) {
try{
mStudent.wait();//释放锁,下次被唤醒的时候就从这里醒过来
}catch (InterruptedException e){
e.printStackTrace();
}
}
if (mFlag) {
mStudent.name = "Leo";
mStudent.age = 24;
} else {
mStudent.name = "Franz";
mStudent.age = 25;
}
mFlag = !mFlag;
mStudent.flag = true;
mStudent.notify();//唤醒之后并不表示被唤醒线程立马拥有执行权,而是进入等待队列争夺执行权。
}
}
}
}
class GetTask implements Runnable {
private Student mStudent;
public GetTask(Student mStudent) {
this.mStudent = mStudent;
}
@Override
public void run() {
while (true) {
synchronized(mStudent) {
if(!mStudent.flag) {
try{
mStudent.wait();
}catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(mStudent.name + " " + mStudent.age);
mStudent.flag = false;
mStudent.notify();
}
}
}
}
class Student {
String name;
int age;
boolean flag;
}
public class TestClass {
public static void main(String[] args) {
Student student = new Student();
SetTask task1 = new SetTask(student);
GetTask task2 = new GetTask(student);
Thread th1 = new Thread(task1);
Thread th2 = new Thread(task2);
th1.start();
th2.start();
}
}//其实上述代码如果把Student的成员设置为私有,
//将Runnable中的方run()方法封装到Student中,并用synchronized修饰,更加规范.
//注意线程被notify的地方就是当初wait的后一行语句;
改进版本的代码如下:
public synchronized void changeName(String name,int age) {
if (mTimes < 300) {
if (flag) {
setName(name);
setAge(age);
mTimes++;
flag = false;
notify();
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public synchronized void showName() {
if (!flag) {
System.out.println("Get student name: " + getAge() + getName());
flag = true;
notify();
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void run() {
while(true) {
if(mStudent.mTimes % 2 == 0) {
mStudent.changeName("Leonardo",24);
}else {
mStudent.changeName("Hugo",25);
}
}
}
public void run() {
while(true) {
mStudent.showName();
}
}
至此,线程生命周期就可以再次深化,以下附上图解:
线程组
Java中使用ThreadGroup来管理一组线程,并允许直接对线程组进行控制。默认情况下所有线程属于主线程组public final ThreadGroup getThreadGroup()
Task task = new Task();
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
System.out.println(thread1.getThreadGroup().getName());
System.out.println(thread2.getThreadGroup().getName());
//默认都属于main线程
可以将新建的线程归类到特定线程组中
ThreadGroup threadGroup = new ThreadGroup("Leonado Hugo up");
Task task = new Task();
Thread th1 = new Thread(threadGroup, task,"线程1");
Thread th2 = new Thread(threadGroup, task,"线程2");
System.out.println(th1.getThreadGroup().getName());
//输出我会有二猫
线程组可以对线程进行批处理,比如threadGroup.setMaxPriority(7);不过开发中使用较少。
线程池
程序启动一个线程成本比较高,因为涉及到与操作系统交互,至少需要考虑线程的创建、销毁和线程切换带来的系统损耗,使用线程池能够很好地提高性能,因为线程池中的每一个线程代码执行完后并不会死亡,而是回到池中变成空闲状态,等待其他对象来调用。当程序中使用大量生存期很短的线程时,更应该使用线程池。 线程池的设计就是采用生产者-消费者模式,线程池里面的线程是消费者,我们塞给线程池的任务是生产者。可以理解成线程池就是火车站售票厅,线程池里面的线程就是火车站售票厅窗口员工,我们去买票或者退票改签就是给窗口员工任务也就是生产,然后窗口员工帮我们办理业务,也就是消费。 一般我们是用ThreadPoolExecutor来创建线程池,我找了里面参数最多的构造器。
里面的参数含义自己查阅api就知道了,根据构造器的定义,解释下线程池的工作原理:
当用户抛给线程池一堆任务的时候,线程池优先把任务抛给corepool中的核心线程执行,当核心线程都在忙碌,再提交的新任务,如果没有其他空闲线程,会存放到阻塞队列中,阻塞队列中的任务会等待空闲的线程,出现空闲的线程就会出队执行任务(先进先出顺序执行)。当阻塞队列存满后,再添加任务,才会开辟新的线程(执行最新的任务),如果线程数量达到最大线程数了,拒绝handler就会派上用场。
有四种默认的线程拒绝策略:
- DiscardPolicy 直接抛弃该任务
- AbortPolicy 拒绝该任务并抛出一个RejectedExecutionException
- CallerRunsPolicy 在调用者线程中执行任务
- DiscardOldestPolicy 抛弃最老的未执行任务并尝试再次执行 当然开发者也可以自行实现一套线程的拒绝策略。
JDK5新增了一个Executors工厂类来产生线程池,有如下几个方法
public static ExecutorService newCachedThreadPool()//可以无限扩大的线程池
public static ExecutorService newFixedThreadPool(int nThreads)
public static ExecutorService newSingleThreadExecutor()
这些方法的返回值是ExecutorService对象,该对象表示一个线程池,可以执行Runnable对象或者Callable对象代表的线程。它提供了如下方法
Future<?> submit(Runnable task)
<T> Future<T> submit(Callable<T> task)
//示例代码
ExecutorService pool = Executors.newFixedThreadPool(3);
// is a ThreadPoolExecutor exactly, implement of ExecutorService
pool.submit(new OutTask());
pool.submit(new OutTask());
//ThreadPoolExecutor还有execute方法提交一个Runnable任务
pool.shutdown();
//Initiates an orderly shutdown in which previously submitted tasks are executed, but no new tasks will be accepted.
//若没有这个方法,线程就会保留而不结束。
再看一个例子:
ExecutorService executorService = Executors.newFixedThreadPool(1);
executorService.submit(new Runnable() {
@Override
public void run() {
printExecutionThreadStatus("run方法");
}
});
System.out.println("已经提交run task");
Future<String> future = executorService.submit(new Callable<String>() {
@Override
public String call() throws Exception {
printExecutionThreadStatus("call方法");
System.out.println("执行call任务ing");
Thread.sleep(TimeUnit.SECONDS.toMillis(5));
System.out.println("执行call任务结束");
return "这是call返回的结果";
}
});
System.out.println("已经提交call task");
try {
System.out.println("开始获取call task的执行结果");
String result = future.get();//这个方法调用者会阻塞,直到得到返回值或者出现异常
System.out.println("获取到了任务执行的结果,为:" + result);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println("任务提交完毕");
// TODO 关闭线程池,shutdown方法会等待所有已经提交的任务完成,然后释放所有资源
System.out.println("关闭线程池");
executorService.shutdown();
shutdown和shutDownNow方法的区别
- shutdown关闭一个线程池,不再接收新任务,中断没有执行任务的线程,但对于已经提交的任务,会等待其完成后再关闭
- shutdownNow会关闭所有线程,但这不算是可以保证结果的方法,它会给运行中线程发送interrupt指令,如果线程不响应则中断失败
submit和execute方法的区别
- submit方法会返回Future对象,意味着调用者希望获得任务的返回值
- execute方法就是发射后不管,将任务抛给线程池执行
线程池最大线程数配置
- 如果线程池主要处理cpu密集型的任务,则设置最大线程数为cpu核心数+1
- 如果线程池主要处理IO密集型的任务,则配置机器的cpu核心数*2 因为IO操作的速度远比CPU慢,这样设置能有效利用CPU空闲的时间 线程池中独有的创建新线程的方法
public class TestClass {
static class CallableTask implements Callable<Integer> {
private Integer number;
public CallableTask(Integer number) {
this.number = number;
}
@Override
public Integer call() throws Exception {
return number * 2;
}
}
//Callable是有返回值的,其泛型就是call方法的返回值类型。
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(3);
Future<Integer> result1 = pool.submit(new CallableTask(200));
Future<Integer> result2 = pool.submit(new CallableTask(100));
System.out.println(result1.get());//该方法用于获取线程执行结果
System.out.println(result2.get());
pool.shutdown();
}
}
注: IO操作如读写文件,访问网络,基本是不耗cpu的,CPU通过DMA的机制,给相应的处理组件发送命令,然后这些组件把任务完成后能够把消息回调给CPU
阻塞队列
阻塞队列与普通队列的区别在于,当队列是空的时,从队列中获取元素的操作将会被阻塞,或者当队列是满时,往队列里添加元素的操作会被阻塞。试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列插入新的元素。同样,试图往已满的阻塞队列中添加新元素的线程同样也会被阻塞,直到其他的线程使队列重新变得空闲起来,如从队列中移除一个或者多个元素,或者完全清空队列 Java API 提供了一些阻塞队列的实现,其中有有界和无界之分,区别在于队列有没有容量大小的限制,但这仅仅是对插入上限制对区别,拿取任务仍然是会被阻塞的。
从5.0开始,JDK在java.util.concurrent包里提供了阻塞队列的官方实现。
public class BlockingQueue {
private List queue = new LinkedList();
private int limit = 10;
public BlockingQueue(int limit){
this.limit = limit;
}
public synchronized void enqueue(Object item)
throws InterruptedException {
while(this.queue.size() == this.limit) {
wait();
}
if(this.queue.size() == 0) {
notifyAll();
}
this.queue.add(item);
}
public synchronized Object dequeue()
throws InterruptedException{
while(this.queue.size() == 0){
wait();
}
if(this.queue.size() == this.limit){
notifyAll();
}
return this.queue.remove(0);
}
}
定时器
定时器用于调度单个或多个定时任务单次或多次地执行,由Timer和TimerTask结合使用,不过开发中一般不用他们,而是使用Quartz这个开源框架。 每一个Timer对应单个后台线程。当计时器所有任务执行完毕后,会被当做垃圾回收,但这可能花掉较长的时间——
After the last live reference to a Timer object goes away and all outstanding tasks have completed execution, the timer's task execution thread terminates gracefully (and becomes subject to garbage collection). However, this can take arbitrarily long to occur.
须知Timer对应的线程又不是守护线程,当用户想尽快结束该线程,应调用其cancel方法。 Timer是线程安全的,多个线程可以共享单个Timer而无需进行外部同步。 此类不提供实时保护,它使用Object.wait()来安排任务。 Timer有其固有的缺陷,
- 有且仅有一个线程去执行定时任务,如果存在多个任务,切任务时间过长,会导致执行效果与预期不符——
Timer tasks should complete quickly. If a timer task takes excessive time to complete, it "hogs" the timer's task execution thread. This can, in turn, delay the execution of subsequent tasks, which may "bunch up" and execute in rapid succession when (and if) the offending task finally completes.
- 如果TimerTask抛出RuntimeException,Timer会停止所有任务的运行
Timer有很多构造器,简单用法如下:
Timer timer = new Timer();
timer.schedule(new TaskPackage(),2000);
//你会发现即使不是循环的任务,该线程也很可能不会再执行完后结束,
//需要手动调用cancel方法或者System.gc()。
CAS原理
说到CAS,得先提到原子操作这个概念——原子(atomic)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意为“不可被中断的一个或一系列操作”。即要么完全不被中断地执行,要么完全不执行。 在并发编程中,我们需要将一些指令设定为原子操作,以避免并发修改变量引起的同步问题。 在java中,我们常见的a++不是原子操作,一个简单的a++操作涉及到三个操作,获取变量a的内存值,将变量a+1,将新值写入内存,这里涉及到了两次内存访问,如果在多线程环境下,那么会出现并发安全问题。 实现原子操作,在Java中可以通过锁和循环CAS的方式。锁上面已经说了很多,synchronized就是锁。而现在介绍CAS,其全程为compare and swap,即比较并交换,我们假设内存中的内存地址V,旧的预期值A,要修改的新值B。一个CAS涉及如下的操作,
1、比较 A 与 V 是否相等。(比较)
2、如果比较相等,将 B 写入 V。(交换)
3、返回操作是否成功。
如果比较时A与V不相等,则自循环重新比较,直到成功为止。当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线程只会收到操作失败的信号。可见一次CAS操作是原子操作。其底层原理是利用了现代处理器提供的CMPXCHG(Compare and Exchange)指令实现。
实现流程图如下所示:
如果还是比较抽象,可以看下面的步骤示例
CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。
1.在内存地址V当中,存储着值为10的变量。
2.此时线程1想要把变量的值增加1。对线程1来说,旧的预期值A=10,要修改的新值B=11。
3.在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。
4.线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,提交失败。
5.线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。
6.这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的。
7.线程1进行SWAP,把地址V的值替换为B,也就是12。
参考: www.jianshu.com/p/ae25eb3cf…
CAS由于不存在上下文切换,效率比Synchronized快很多,除非特殊设计的高度竞争的测试环境,CAS机制都比加锁要快,现在JVM的发展方向也是无锁化。
乐观锁和悲观锁
- CAS属于乐观锁,乐观锁就是每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
- synchronized是悲观锁,被一个线程拿到锁之后,其他线程必须等待该线程释放锁,性能较差
CAS的运用
明白了CAS的原理,下面来看看其运用,从Java 1.5开始,JDK的并发包里提供了一些类来支持原子操作,如AtomicBoolean(用原子方式更新的boolean值)、AtomicInteger(用原子方式更新的int值)和AtomicLong(用原子方式更新的long值)。这些原子包装类还提供了有用的工具方法,比如以原子的方式将当前值自增1和自减1。
AtomicInteger 的 incrementAndGet()
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
CAS的问题
- ABA问题。因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。从Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference、AtomicMarkableReference来解决ABA问题。AtomicStampedReference这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。 AtomicMarkableReference只关心变量有没有被动过,AtomicStampedReference不仅关心有没有被动过,也关心被改动过几次
public boolean compareAndSet(
V expectedReference, // 预期引用
V newReference, // 更新后的引用
int expectedStamp, // 预期标志
int newStamp // 更新后的标志
)
-
循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销,特别在高并发情况下性能明显下降。如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升。pause指令有两个作用:第一,它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零;第二,它可以避免在退出循环的时候因内存顺序冲突(Memory Order Violation)而引起CPU流水线被清空(CPU Pipeline Flush),从而提高CPU的执行效率。
-
只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。
AQS
是用来构建锁或者其他同步组件的基础框架,比如ReentrantLock、ReentrantReadWriteLock和CountDownLatch就是基于AQS实现的。它使用了一个int成员变量state表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。它是CLH队列锁的一种变体实现。
CLH队列:每个线程被封装成一个node节点,有指向前一个节点的引用,通过不断的自旋判断前一个节点有没有释放锁(标志位变化),释放后当前线程就能获得这把锁,AQS优化了CLH的实现,使用了双向链表,使得当一个线程释放锁之后,能唤醒下一链表元素,并且对每个节点的自旋次数做了限制,超过一定次数就把自己阻塞
AQS的主要使用方式是继承,子类通过继承AQS并实现它的抽象方法来管理同步状态,同步器的设计基于模板方法模式,所以如果要实现我们自己的同步工具类就需要覆盖其中几个可重写的方法,如tryAcquire、tryReleaseShared等等。
这样设计的目的是同步组件(比如锁)是面向使用者的,它定义了使用者与同步组件交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。这样就很好地隔离了使用者和实现者所需关注的领域。
在内部,AQS维护一个共享资源state,通过内置的FIFO来完成获取资源线程的排队工作。该队列由一个一个的Node结点组成,每个Node结点维护一个prev引用和next引用,分别指向自己的前驱和后继结点,构成一个双端双向链表。
公平锁:多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
优点:所有的线程都能得到资源,不会饿死在队列中。
缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
非公平锁:多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。
可重入锁:的字面意思是“可以重新进入的锁”,即允许同一个线程多次获取同一把锁。比如一个递归函数里有加锁操作,
递归过程中这个锁会阻塞自己吗?如果不会,那么这个锁就是可重入锁(因为这个原因可重入锁也叫做递归锁)。Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的。
JMM
cpu执行一个指令只需要0.6纳秒,执行一个读取操作需要大概100纳秒,可见一次常规运算中cpu大部分时间都消耗在了读取内存,由此现代CPU带高速缓存区域,一般一个core带三层缓存,不同层的读取速度和缓存容量不一样,不同core之间还有共享缓存,这样能提高cpu运算时候的效率,减少读取内存的时间
Java虚拟机中分工作内存和主内存,工作内存主要分布在cpu寄存器和三级缓存,主内存主要分布在RAM中,Java程序运行时,每个线程独占一个工作内存,不同线程之间工作内存不能互相访问,且线程不能直接访问主内存,需要通过一个save和load操作从主内存中读写内容,如对于同一个变量,不同线程从主内存中获取其副本,在自己的工作内存中操作。
JMM但设计模型有效利用了现代cpu的缓存策略,但这样就造成了多线程之间数据同步问题,最简单的一个count = count + 1操作,两个线程同时运行可能最终count的值是1而不是2。
综上问题,就引出了我们常说的可见行和原子性的概念,由此Java有volatile、synchronized关键字。
volatile: 实现机理是强迫线程获取变量时候一定从主内存中获取最新值,修改了变量后强制立即刷新到主内存中,且禁止指令重排序(重排指cpu为了提高运算性能可能会把计算步骤修改),这样就保证了可见性。 但对于非原子性但操作,volatile就不能保证并发安全,因为不是每一步操作都是从内存中获取最新都变量值,上下文切换的时候很容易造成变量值修改异常。
synchronized:关键字能真正保证可见性和原子性
《thinking in Java》的第21章的《并发》有写:“除了long和double类型,Java基本数据类型都是的简单读写都是原子的,而简单读写就是赋值和return语句。”因此而对于其他自加自减以及其他运算操作,是非原子操作。
这段话是针对32bit的JVM说明的,因为long和double类型占64bit,在32bit的机器上不能一步做完读写操作,但对于64bit的机器,Java所有基本数据类型的读写都是原子操作。
但是,虽然读写基本类型是原子的,但其都是在工作内存层面的,由于线程有一个时间分片的概念,并不能保证此时基本类型的数据对于其他线程来说是最新修改值,因此,声明为volative可以保证可视性。所以实际开发中,对于64bit机器基本变量最基本的读和写,即get和set,或者count = 4,可以用volatile关键字保证原子性和可见性。
注意:x = y这样的操作不是简单读写,它包含了读取y的值和把y的值赋予x两步操作,只有x = 10才是简单读写
volatile的实现原理
volatile关键字修饰的变量会存在一个“lock:”的前缀 Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。 同时该指令会将当前处理器缓存行的数据直接写会到系统内存中,且这个写回内存的操作会使在其他CPU里缓存了该地址的数据无效,使其不得不重新从主存中获取。
synchronized实现原理
Synchronized在JVM里的实现都是基于进入和退出Monitor对象来实现方法同步和代码块同步,虽然具体实现细节不一样,但是都可以通过成对的MonitorEnter和MonitorExit指令来实现。 对同步块,MonitorEnter指令插入在同步代码块的开始位置,而monitorExit指令则插入在方法结束处和异常处,JVM保证每个MonitorEnter必须有对应的MonitorExit。总的来说,当代码执行到该指令时,将会尝试获取该对象Monitor的所有权,即尝试获得该对象的锁
synchronized使用的锁是存放在Java对象头里面,Java对象的对象头由 mark word 和 klass pointer 两部分组成:
1)mark word存储了同步状态、标识、hashcode、GC状态等等
2)klass pointer存储对象的类型指针,该指针指向它的类元数据
锁信息则是存在于对象的mark word中,MarkWord里默认数据是存储对象的HashCode等信息,
但是会随着对象的运行改变而发生变化,不同的锁状态对应着不同的记录存储方式
一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争情况逐渐升级。锁可以升级但不能降级,目的是为了提高获得锁和释放锁的效率。早期synchronized关键字都是通过重量级锁,即阻塞线程进行上下文切换实现的同步功能。从jdk1.6开始进行锁的优化
偏向锁
引入背景:大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁,减少不必要的CAS操作。 偏向锁,顾名思义,它会偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,减少加锁/解锁的一些CAS操作(比如等待队列的一些CAS操作),这种情况下,就会给线程加一个偏向锁。 如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁(撤销偏向锁的时候会导致stop the word,即暂停所有线程,把当前任务完成后继续其他任务),将锁恢复到标准的轻量级锁。它通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。
在有锁的竞争时,偏向锁会多做很多额外操作,尤其是撤销偏向所的时候会导致进入安全点,安全点会导致stw,导致性能下降,这种情况下应当禁用。 jvm开启/关闭偏向锁 开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 关闭偏向锁:-XX:-UseBiasedLocking
轻量级锁
轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁,轻量级锁通过自旋机制实现,所谓自旋就是指如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。但是线程自旋是需要消耗CPU的,说白了就是让CPU在做无用功,线程不能一直占用CPU自旋做无用功,所以需要设定一个自旋等待的最大时间。如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态,即升级为重量级锁。
自旋的优点:自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起操作的消耗!但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用cpu做无用功,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要cup的线程又不能获取到cpu,造成cpu的浪费。
自旋时间阈值的确定:JVM对于自旋次数的选择,jdk1.5默认为10次,在1.6引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间。JDK1.6中-XX:+UseSpinning开启自旋锁; JDK1.7后,去掉此参数,由jvm控制;
不同锁的比较
synchronized和lock的选择
在一些内置锁无法满足需求的情况下,ReentrantLock可以作为一种高级工具。当需要一些高级功能时才应该使用ReentrantLock,这些功能包括:可定时的,可轮询的与可中断的锁获取操作,公平队列,以及非块结构的锁。否则,还是应该优先使用Synchronized
常见多线程面试题
sychronied修饰普通方法和静态方法的区别?什么是可见性?
对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。 但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,类锁其实锁的是每个类的对应的class对象。类锁和对象锁之间也是互不干扰的。 可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。 由于线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量,那么对于共享变量V,它们首先是在自己的工作内存,之后再同步到主内存。可是并不会及时的刷到主存中,而是会有一定时间差。很明显,这个时候线程 A 对变量 V 的操作对于线程 B 而言就不具备可见性了 。 要解决共享对象可见性这个问题,我们可以使用volatile关键字或者是加锁。
锁分哪几类?
synchronized非公平锁实现 读写锁里面的读锁是个典型的共享锁,写锁是排他锁
CAS无锁编程的原理。
使用当前的处理器基本都支持CAS()的指令,只不过每个厂家所实现的算法并不一样,每一个CAS操作过程都包含三个运算符:一个内存地址V,一个期望的值A和一个新值B,操作的时候如果这个地址上存放的值等于这个期望的值A,则将地址上的值赋为新值B,否则不做任何操作。 CAS的基本思路就是,如果这个地址上的值和期望的值相等,则给其赋予新值,否则不做任何事儿,但是要返回原值是多少。循环CAS就是在一个循环里不断的做cas操作,直到成功为止。 还可以说说CAS的三大问题。 ReentrantLock的实现原理。 线程可以重复进入任何一个它已经拥有的锁所同步着的代码块,synchronized、ReentrantLock都是可重入的锁。在实现上,就是线程每次获取锁时判定如果获得锁的线程是它自己时,简单将计数器累积即可,每 释放一次锁,进行计数器累减,直到计算器归零,表示线程已经彻底释放锁。 底层则是利用了JUC中的AQS来实现的。
AQS原理 (小米 京东)
是用来构建锁或者其他同步组件的基础框架,比如ReentrantLock、ReentrantReadWriteLock和CountDownLatch就是基于AQS实现的。它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。它是CLH队列锁的一种变体实现。它可以实现2种同步方式:独占式,共享式。 AQS的主要使用方式是继承,子类通过继承AQS并实现它的抽象方法来管理同步状态,同步器的设计基于模板方法模式,所以如果要实现我们自己的同步工具类就需要覆盖其中几个可重写的方法,如tryAcquire、tryReleaseShared等等。 这样设计的目的是同步组件(比如锁)是面向使用者的,它定义了使用者与同步组件交互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。这样就很好地隔离了使用者和实现者所需关注的领域。 在内部,AQS维护一个共享资源state,通过内置的FIFO来完成获取资源线程的排队工作。该队列由一个一个的Node结点组成,每个Node结点维护一个prev引用和next引用,分别指向自己的前驱和后继结点,构成一个双端双向链表。 Synchronized的原理以及与ReentrantLock的区别。(360) synchronized (this)原理:涉及两条指令:monitorenter,monitorexit;再说同步方法,从同步方法反编译的结果来看,方法的同步并没有通过指令monitorenter和monitorexit来实现,相对于普通方法,其常量池中多了ACC_SYNCHRONIZED标示符。 JVM就是根据该标示符来实现方法的同步的:当方法被调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
Synchronized做了哪些优化 (京东)
引入如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁、逃逸分析 等技术来减少锁操作的开销。 逃逸分析 如果证明一个对象不会逃逸方法外或者线程外,则可针对此变量进行优化: 同步消除synchronization Elimination,如果一个对象不会逃逸出线程,则对此变量的同步措施可消除。 锁消除和粗化 锁消除:虚拟机的运行时编译器在运行时如果检测到一些要求同步的代码上不可能发生共享数据竞争,则会去掉这些锁。 锁粗化:将临近的代码块用同一个锁合并起来。 消除无意义的锁获取和释放,可以提高程序运行性能。
Synchronized static与非static锁的区别和范围(小米)
对象锁是用于对象实例方法,或者一个对象实例上的,类锁是用于类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。 但是有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,类锁其实锁的是每个类的对应的class对象。类锁和对象锁之间也是互不干扰的。
volatile 能否保证线程安全?在DCL上的作用是什么?
不能保证,在DCL的作用是:volatile是会保证被修饰的变量的可见性和 有序性,保证了单例模式下,保证在创建对象的时候的执行顺序一定是 1.分配内存空间 2.实例化对象instance 3.把instance引用指向已分配的内存空间,此时instance有了内存地址,不再为null了 的步骤, 从而保证了instance要么为null 要么是已经完全初始化好的对象。 volatile和synchronize有什么区别?(B站 小米 京东) volatile是最轻量的同步机制。 volatile保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。但是volatile不能保证操作的原子性,因此多线程下的写复合操作会导致线程安全问题。 关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性,又称为内置锁机制。
什么是守护线程?你是如何退出一个线程的?
Daemon(守护)线程是一种支持型线程,因为它主要被用作程序中后台调度以及支持性工作。这意味着,当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出。可以通过调用Thread.setDaemon(true)将线程设置为Daemon线程。我们一般用不上,比如垃圾回收线程就是Daemon线程。 线程的中止: 要么是run执行完成了,要么是抛出了一个未处理的异常导致线程提前结束。 暂停、恢复和停止操作对应在线程Thread的API就是suspend()、resume()和stop()。但是这些API是过期的,也就是不建议使用的。因为会导致程序可能工作在不确定状态下。 安全的中止则是其他线程通过调用某个线程A的interrupt()方法对其进行中断操作,被中断的线程则是通过线程通过方法isInterrupted()来进行判断是否被中断,也可以调用静态方法Thread.interrupted()来进行判断当前线程是否被中断,不过Thread.interrupted()会同时将中断标识位改写为false。
sleep 、wait、yield 的区别,wait 的线程如何唤醒它?(东方头条) yield()方法:使当前线程让出CPU占有权,但让出的时间是不可设定的。也不会释放锁资源。所有执行yield()的线程有可能在进入到就绪状态后会被操作系统再次选中马上又被执行。 yield() 、sleep()被调用后,都不会释放当前线程所持有的锁。 调用wait()方法后,会释放当前线程持有的锁,而且当前被唤醒后,会重新去竞争锁,锁竞争到后才会执行wait方法后面的代码。 Wait通常被用于线程间交互,sleep通常被用于暂停执行,yield()方法使当前线程让出CPU占有权。 wait 的线程使用notify/notifyAll()进行唤醒。
sleep是可中断的么?(小米)
sleep本身就支持中断,如果线程在sleep期间被中断,则会抛出一个中断异常。
线程生命周期 Java中线程的状态分为6种:
- 初始(NEW):新创建了一个线程对象,但还没有调用start()方法。
- 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。 线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。 3. 阻塞(BLOCKED):表示线程阻塞于锁。 4. 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。 5. 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。
- 终止(TERMINATED):表示该线程已经执行完毕。
ThreadLocal是什么?
ThreadLocal是Java里一种特殊的变量。ThreadLocal为每个线程都提供了变量的副本,使得每个线程在某一时间訪问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。 在内部实现上,每个线程内部都有一个ThreadLocalMap,用来保存每个线程所拥有的变量副本。 线程池基本原理。 在开发过程中,合理地使用线程池能够带来3个好处。 第一:降低资源消耗。第二:提高响应速度。第三:提高线程的可管理性。 1)如果当前运行的线程少于corePoolSize,则创建新线程来执行任务(注意,执行这一步骤需要获取全局锁)。 2)如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。 3)如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务。 4)如果创建新线程将使当前运行的线程超出maximumPoolSize,任务将被拒绝,并调用RejectedExecutionHandler.rejectedExecution()方法。
有三个线程T1,T2,T3,怎么确保它们按顺序执行?
可以用join方法实现。