线程/线程池
线程状态及其转换?
Java创建线程的几种方式?
- 1.继承Thread并重写run()方法
其实Thread也实现了Runnbale接口 - 2.实现Runnable接口并重写run()方法
实现Runnable接口并重写run()方法,然后将Runnable对象传递给Thread的构造函数来创建新线程 - 3.实现Callable接口
常结合Future用于创建与返回值的线程
…待总结 - 4.使用线程池
Java线程终止的几种方式?
-
1.执行完run()或者call()方法自动退出
-
2.抛出未捕获的Exception或者错误
-
3.stop(不推荐)
这个方法是不安全的,已被弃用(此外,Thread中被弃用的还有suspend、resume等
)
原因:调用 stop() 方法会立刻停止 run() 方法中剩余的全部工作,且会立即释放线程持有的所有锁
,这可能会导致对象处于不一致状态以银行转账为例,从A账户向B账户转账500元,第一步是从A账户中减去500元,假如到这时线程就被stop了,那么这个线程就会释放它所取得锁,然后其他的线程继续执行,这样A账户就莫名其妙的少了500元而B账户也没有收到钱。这就是stop方法的不安全性
-
4.interupt()方法
interrupt只是设置线程的中断标志位
(这里是终止的含义,不是陷入内核的中断),不会强制线程立即终止,最终是否终止由线程执行决定(也就是可以由编写线程的人访问中断标志位并决定是否要退出)。
Thread类中常用的方法?
public class Thread implements Runnable {
public static native void yield();
public static void sleep(long millis, int nanos);
public synchronized void start();
public void run();
public void interrupt();
public static boolean interrupted();
private native boolean isInterrupted(boolean ClearInterrupted);
public final native boolean isAlive();
public final void join();
public final void stop(); //弃用
public void destroy(); //弃用
public final void suspend(); //弃用
public final void resume(); //弃用
}
sleep()与wait()的区别?
1. sleep是Thread的方法; wait是Object的方法;
2. sleep不会释放锁; wait会释放锁
;
3. 等待时间不同…
…
线程有哪些状态及其转换?
Executor框架
Execotor与ExecutorService工作原理?
- Executor
Executor是一多线程框架,它将任务的提交
与执行
解耦 => 也就是说,执行execute()时仅仅提交任务到队列,并不一定立即开始执行
…public interface Executor { // 并不代表立即执行线程,只是放入队列 void execute(Runnable command); }
- ExecutorService
1. 为了解决执行服务的生命周期
问题,在Executor的基础上添加了一些有用的生命周期管理方法;
2. 为了方便提交具有返回值的任务
,新增了submit()方法 => 详见Callable与Future
综上,形成了ExecutorService接口public interface ExecutorService extends Executor { void shutdown(); List<Runnable> shutdownNow(); boolean isShutdown(); boolean isTerminated(); <T> Future<T> submit(Callable<T> task); <T> Future<T> submit(Runnable task, T result); Future<?> submit(Runnable task); <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException; ..... }
Callable与Future?
- Callable
为了解决Runnbale中的run()方法不能抛出异常、没有返回值的情况
=>
定义了Callable接口:public interface Callable<V> { V call() throws Exception; }
- Future
Future表示一个任务的生命周期
,并提供相应的方法来判断是否已经完成或取消,以及获取任务的结果
和取消任务等。接口如下:
注意get()方法,如果任务已经完成,那么get会返回结果;否则public interface Future<V> { boolean cancel(boolean mayInterruptIfRunning); boolean isCancelled(); boolean isDone(); V get() throws InterruptedException, ExecutionException; V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException; }
get会阻塞直到任务完成
Future的具体使用方法可参考Executor接口…
介绍四种线程池?
- 线程池使用示例
ExecutorService threadPool=Executors.newXXXThreadPool();
- 1.newFixedThreadPool
1.将创建一个固定数量的线程池
,每提交一个任务时就创建一个线程,直到达到最大数量;
2.如果有线程因为异常而结束,将会创建新的线程以补足数量;
3.线程数量不足时,任务将在队列中等待。 - 2.newCachedThreadPool
1.如果当前线程数超过了任务数,会回收空闲线程;
2.如果当前线程数量不足,会创建新线程,且线程数量不受限制。
=> 实在不理解cache在此处的含义??? 只需记住它可以根据需求动态调整线程数量
!! - 3.newScheduledThreadPool
创建一个给定数量的线程池,且以延迟或定时的方式来执行任务
- 4.newSingleThreadExecutor
创建一个单线程的线程池
,能确保依照任务在队列中的顺序来串行执行
创建线程池时的重要参数?
饱和策略?
并发/同步
Java同步的核心技术—管程(待完成)
参考资料:管程:并发编程的基石
Java多线程–Monitor对象(一)
并发涉及的三个特性(待完善)
- 原子性
1.synchronized通过lock/unlock操作(虚拟机层面就是monitorenter、monitoerexit)完成保证原子性;
2.volatile不能保证原子性 - 可见性
1.synchronized通过规则:对一个变量执行unlock操作之前,必须先将此变量同步回主内存来实现可见性
2.volatile通过规则…抱保证可见性
3.final由于不可修改,无需同步,直接保证了可见性 - 有序性
1.sunchronized通过规则:一个变量在同一时刻只允许一个线程对进行lock操作
保证有序性
2.volatile通过规则…保证有序性 - 备注
1.volatile只能保证可见性、有序性,无法保证原子性
=> 原子性需要通过加锁来保证!
2.final也能提供一定程度上的原子性,因为final的不可变性
使共享变量不能被修改,没有线程安全问题,他对共享变量的访问等价意义上是原子的
什么是线程安全?
-
绝对线程安全
代码本身封装了所有必要的正确性保证手段(如同步互斥),调用者无需关心多线程下的调用问题,更无需自己实现任何措施来保证多线程环境下的正确调用. -
相对线程安全
绝对线程安全很难实现,java中绝大部分线程安全都是说的相对线程安全 =>只能保证对象的单次调用是线程安全的
比如:对于一个Vector对象,一个线程进行remove(i),一个线程执行get(i),可能在某个时刻T1先获得i => T2线程remove(i) => T3线程get(i)则会出现问题。可见Vector的线程安全仅仅只能保证单次调用remove/get是安全的!
线程安全的实现方法有哪些?
阻塞同步(synchronized、Lock)
阻塞同步意味和已获得共享变量的线程会尚未阻塞未获得共享变量的线程 => 这是一种悲观的并发策略
- synchronized
1.基于monitorenter、monitorexit指令,持有共享对象后会将锁的计数值加1,退出时将锁的计数值减1;
2.可重入的;
3.持有锁的时候会无条件阻塞后面其他线程的进入,不需要显示地释放锁,Java语言自动释放;
4.重量级的
,因为JVM使用操作系统的原生线程,阻塞后续进入的线程会导致用户态到内核态切换,开销较大
5.非公平的锁; - Lock接口
1.最重要的实现时ReentrantLock,它也是可重入的;
2.相对于synchronized而言增加了一些高级功能; - ReentrantLock相对于synchronized的高级特性
1.等待可中断:正在等待的线程可以选择放弃等待,转而处理其他事情;
2.可以实现公平锁:虽然默认非公平,但是可以指定reentrantlock为公平的,只是性能会下降;
3.锁绑定多个条件
:在synchronized中,所对象的wait()和notify()方法配合可以实现一个隐含的条件,如果要和多于一个的条件关联,就得额外添加锁;而一个ReentrantLock对象可以同时绑定多个Condition对象
非阻塞同步(CAS)
不断进行冲突检测,如果没有其他线程争用共享变量,则使用它;不需要阻塞线程,这是一种乐观的并发策略;基于处理器的特殊指令实现,典型的是test-and-set、compare-and-swap
- CAS
CAS(V,A,B),如果内存地址V处的值与A值相等,则将V处的值修改为B
,这是一个原子操作
基本思路就是循环检测V处的值是否被修改for(;;){ int current=get(); // 旧值A(预期值) int next=current+1; // 新值B if(compareAndSet(current,next)) .... }
- CAS导致ABA问题
T1线程对共享变量进行了A-> B-> A的修改,但是T2线程恰好只看到了两次A
=> 大部分请求下ABA不会影响程序并发的正确性,如果确实影响了,可通过版本号机制解决
版本号机制
:每次修改数据时跟新版本号
,在上面的例子中,T2第一次看到A的版本号与第二次看到A的版本号不同…
无同步(可重入代码、threadlocal、final)
- 可重入代码
可重入代码天生就是线程安全的,它不需要同步; - 线程本地存储
ThreadLocal保证线程内的代码共享该数,但是线程间不可共享数据 - final
final修饰的基本数据类型时不可变,不会有同步问题;(final修饰的对象只是引用不可变,要保证对象不可变还需更多限制…)
Java中同步技术包括哪些?(待完善)
volatile
-
volatile不能保证保证原子性
修改volatile变量大概分为四步:
1)数据读取到寄存器
2)修改变量值
3)修改后的值写回(到寄存器或者工作内存)
4)插入内存屏障,即lock指令,让其他线程可见前三步都是不安全的,取值和写回之间,不能保证没有其他线程修改。
原子性需要锁来保证
。 -
volatile保证可见性
主要通过两条规则保证可见性:
1.变量修改后立即
将新值同步回主内存(普通变量虽然会同步回主内存但不是立即!);
2.使用前立即
从主内存刷新
注意:可见性是说变量第一时间同步到主内存,但并不能保证变量更新后立即被其他线程感知到,因为更新操作不一定是原子的,可能还没有执行同步到主存的指令…
如何实现的: 修改执行完成后,插入cpu指令lock addl $0x0,(%esp)
=> 它的作用是:将本处理器的缓存写入内存… -
volatile保证有序性
volatile禁止指令重排序,从而保证了有序性
如何实现的: 修改执行完成后,插入cpu指令lock addl $0x0,(%esp)
=> 由于指令重排序必须保证有依赖数据的指令能得到正确的数据,对于lock addl $0x0,(%esp)而言,要得到正确的数据,则必须要先完成变量的正确修改(就不能在lock前重排序),从而自动保证了变量没有重排序 (似乎没有解释清楚??) -
其他知识点
1.被volatile关键字修饰的对象作为类变量或实例变量时,其对象中携带的类变量和实例变量也相当于被volatile关键字修饰了
=> 参考volatile关键字修饰对象是什么效果?
2.volatile只能保证可见性、有序性;而snchronized三者都能保证!
synchronized
- synchronized使用方法
修饰普通方法:这个时候锁住的是对象
修饰静态方法:这个时候锁住的是类
=> 对象仍然可以调用普通方法
修饰代码块:synchronized(A.class)锁住的是类
;synchronized(obj)锁住的是对象
; - synchronized修饰代码块是如何实现的?
1.通过字节码指令monitorenter、monitorexit报文同步块;
2.虚拟机指令momitorenter尝试获取对象monitor
(存在于每个对象的对象头中
,这也是为什么任意对象可以作为锁的原因);
3.获得monitor对象后将锁计数值+1,退出时-1. - synchronized修饰方法时是如何实现的?
在字节码层面给方法标志ACC_SYNCHRONIZED
,JVM据此执行相应的同步调用
注:无论修饰方法还是代码块,synchronized底层都是依赖操作系统的锁实现的… - synchronized与ReentrantLock的区别?
synchronized依赖于JVM实现,实现原理没有暴露给用户;
ReentrantLock是直接在JDK上实现的,可直接查看源码了解其实现原理 - RentrantLock相对于synchronized的优点?
等待可中断:正在等待的线程可以选择放弃等待,改为处理其他事情
公平锁:默认是非公平的,不过可以指定为公平的
锁绑定多个条件:一个ReentrantLock对象可以同时绑定多个Conditon对象。在synchronized中,锁对象的wait()和notify()方法配合只可以实现一个隐含条件
;而ReentrantLock只需多次调用newCondition()即可绑定多个条件
显式锁
原子变量
ThreadLocal原理?
-
使用举例
// 存储了每个线程的LOCAL class MyThreadLocal{ static final ThreadLocal<Integer> LOCAL=new ThreadLocal<>(); } class MyThread implements Runnable{ String name; Integer id; MyThread(String name, Integer id){ this.name=name; this.id=id; } @Override public void run() { MyThreadLocal.LOCAL.set(id); System.out.println("thread "+id+" set local:"+id); try{ Thread.sleep(100); } catch (InterruptedException e){ e.printStackTrace(); } Integer local=MyThreadLocal.LOCAL.get(); System.out.println("thread "+id+" get local:"+local); } } public class ThreadLocalTest { public static void main(String[] args){ for(int i=0;i<5;i++){ Thread t=new Thread(new MyThread(String.valueOf(i),i)); t.start(); } System.out.println("end!"); } }
即同一个变量,所有线程都可以设置,但是它们设置的只是属于自己的副本 -
概述
从表面上看ThreadLocal相当于维护了一个map,key就是当前的线程,value就是需要存储的对象 => 然而这是不正确的!实际上是ThreadLocal的静态内部类ThreadLocalMap为每个Thread都维护了一个数组table,ThreadLocal确定了一个数组下标,而这个下标就是value存储的对应位置
每个线程持有一个ThreadLocalMap对象。每一个新的线程Thread都会实例化一个ThreadLocalMap并赋值给成员变量threadLocals
-
源码解读!!!
Threadclass Thread implements Runnable { ThreadLocal.ThreadLocalMap threadLocals = null; ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; }
ThreadLocal
public class ThreadLocal<T> { public void set(T value) { Thread t = Thread.currentThread(); // 获取当前线程的变量threadLocals !!! ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); // 会为当前线程创建threadLocals } ThreadLocalMap getMap(Thread t) { return t.threadLocals; } void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); } public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { T result = (T)e.value; return result; } } return setInitialValue(); } public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); } }
ThreadLocalMap => 它是ThreadLocal的静态内部类!
static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } // 存储所有数据! => 不同于HashMap,这里解决冲突使用的是开放定址法 private Entry[] table; private void set(ThreadLocal<?> key, Object value) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i];e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); if (k == key) { e.value = value; return; } if (k == null) { replaceStaleEntry(key, value, i); return; } } // 要插入的元素放入不冲突的位置i tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); } private Entry getEntry(ThreadLocal<?> key) {.....} }
-
总结
1.对于每个看似共享的变量ThrealLocal< T>(如上图的ThreadLocal1、ThreadLocal2);
2.每个线程将它作为Key存储在自己的hash表中
(也就是同一个ThreadLocal对象同时在多个线程的hash表中作为key);
3.ThrealLocal< T>进行get、set时:先获得线程对象 => 然后获得线程自己的hash表 => 然后以ThrealLocal< T>自己为key进行查找...
4.需注意:每个线程自己的ThreadLocalMap是开放定址法实现的hash表,不同于HashMap的拉链法 -
ThreadLocal的内存泄漏问题
根据static class Entry extends WeakReference<ThreadLocal<?>>
=> ThreadLocalMap中使用的key是ThreadLocal的弱引用,而val是强引用
,所以如果ThreadLocal如果没有被外部强引用的时候,key就会被垃圾回收,而val是强引用则无法被回收,从而造成内存泄漏
=> 所以:最好手动清理掉key为null的记录
(调用remove方法)
锁专题
参考资料
一文彻底搞懂面试中常问的各种“锁”
乐观锁
读数据:读数据时总是认为没有其它线程并发地修改数据,所以总是不上锁;
写数据:写数据时需要判断是否有其他线程并发地修改数据…;
两种实现方式:版本号机制、CAS
适用场景:读多写少
举例:JUC.atomic下的原子变量类就是使用CAS实现乐观锁的
悲观锁
读数据:会上锁 => 总是认为有其它线程并发地修改数据,所以要上锁;
写数据:也会上锁;
适用场景:写多读少;
举例:数据库
中的行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁,可看做悲观锁; Java
中的synchronized总是会加锁,属于悲观锁;
自旋锁(有疑问)
概念:若线程T1想要获取一个锁S,但是另一个线程T2尚未释放锁S。于是线程T1不断循环,直到能够获得锁S,这种循环加锁的方式称为自旋锁。
适用场景:(适用)锁的竞争不激烈,线程T1只需要自旋少许时间便可获获得锁,这样能避免T1阻塞再被唤醒的开销;若锁的竞争过于激烈, T1等待时间过长,则会消耗过多的cpu;
举例:Java中哪些是自旋锁??
疑问:什么才是自旋锁?是T1要获取的锁S吗?还是循环等待的过程称为锁???
Synchronized同步锁(synchronized的实现细节待整理)
- 概述
每个Java对象都有一个内部锁
(语言内置的),synchronized使用的就是对象的内部锁完成同步的。 - synchronized的作用范围
1作用于方法时,锁住的是对象的实例(this);
2.作用于静态方法时
,锁住的是Class => 因为Class相关数据存储在永久代,这块区域被共享,因此静态方法上的同步锁相当于类的全局锁,会锁所有调用该方法的线程。 - 举例
synchronized是Java中具体的锁的一种,它可被视为悲观锁、可重入锁、非公平锁
与数据库锁互通的地方
数据库中也有乐观锁、悲观锁。
其他
CAS
见线程安全实现方式…
AQS
什么是可重入?
对于一个锁而言,同一个线程可以反复(递归)的请求并持有它
什么是竞态条件?
当两个或者两个以上的线程共享对统一数据的存取时,数据的修改/访问不一定能达到预期,这取决于线程访问数据的次序
=> 这就是竞态条件
两类典型的竞态条件举例?
-
1.先检查后执行
即通过一个可能已经失效的观测
结果来决定下一步该做什么,典型的就是懒汉式单例:public class Singleton { private Singleton instance; public Singleton getInstance(){ if(instance == null) { //1:读取instance的值 instance = new Singleton(); //2:实例化instance } return instance; } }
可能的情况是线程A发现instance为null,准备创建但是尚未创建对象的时候,线程B也发现instance为null => 于是最终A、B都创建了新对象!
-
2.读取-修改-写入
例如并发地递增一个共享的计数器