线程基础
几个常用的方法
- 休眠线程 sleep()
- 礼让线程 yield()
- 守护线程 setDaemon(boolean b)
- 加入线程 join()
- 设置线程优先级 setPriority()
来个例子:
//主线程中写入这个while循环可以起到等待其他线程都执行完成的效果,而不用取臆测sleep()多长时间;Thread.activeCount() > 2是因为默认两个线程:主线程、GC线程
while(Thread.activeCount() > 2){
Thread.yield();//当前线程礼让其它线程执行
}
线程状态图
![六个状态](https://img-blog.csdn.net/20180430205000975?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3lhbnlpbmduYW4xMzU3/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)
实现多线程的4种方法
- 继承Thread类,重写run()方法,有父类的情况下不能用,因为java单继承。
- 实现Runnable接口,重写run()方法。接口可以多实现。
- 实现Callable接口,重写call()方法,与上一个方法相同但是多了两个特点:
- 可以有返回值,Future对象将会接受它的异步计算结果。执行 Callable 任务后,可以获取一个Future的对象,在该对象上调用get就可以获取到 Callable 任务返回的Object了,再结合线程池接口ExecutorService就可以实现传说中有返回结果的多线程了。
- 可以抛出异常。
- 创建线程池获取线程。
synchronized
- synchronized同步方法
- 由于方法内部的变量具有私有特性,因此它不存在线程安全问题。
- 同步方法所取得的锁都是实例对象。
- synchronized拥有重入的功能,即,当一个线程得到一个锁,也可以调用其它有这个锁的方法或代码块。
- 可重入性可以延伸到子类继承父类的同步方法时也可以调用父类的同步方法。但同步不具有继承性,子类该加上synchronized还是要加上的。
- 当线程出现异常时,锁会自动释放。
- (如果线程A调用同步方法执行了很长时间,线程B必须等待,因此可以用同步代码块优化)
- synchronized同步代码块
- synchronized(this){…}锁对象为当前实例对象
- 任何实例对象都是可以作为锁对象的
- 为多个代码块设置多个非this对象作为锁可以在一定程度上提升运行效率。
- 核心就一句话:学会判断锁对象,一个线程对应一个锁。
- 静态方法锁对象是Xxx.class,这个锁将是类锁,而不再是对象锁,这时它将会对该类的所有实例对象起作用。
- String类型有常量池缓存功能,容易造成死循环,因此常常不用String作为锁对象。
- java1.6之后对synchronized进行了锁优化,效率有了本质上的提高,本质上就是试图在用户态就把加锁问题解决,避免进入内核态的线程阻塞,引入锁升级:偏向锁->轻量级锁->重量级锁(就是默认偏向锁,根据实际情况自动进行锁升级,偏向锁升级到轻量级锁需要等到全局安全点,也就是没有线程在执行字节码时才能执行,因此会有线程阻塞的坑),我们的应用开发中,绝大部分情况下一定会存在 2 个以上的线程竞争,那么如果开启偏向锁,反而会提升获取锁的资源消耗。所以可以通过jvm参数-XX:-UseBiasedLocking 来设置关闭偏向锁。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的,关于偏向锁的撤销,需要等待全局安全点,即在某个时间点上没有字节码正在执行时,它会先暂停拥有偏向锁的线程,然后进行锁升级。
手写一个死锁实例
嵌套同步代码块容易造成死锁,但这并不是死锁的本质,只要相互等待对方释放锁就可能出现死锁
public class DeaThreadTest implements Runnable{
//锁对象用final修饰表示对象必须被初始化,不能被修改;
//非final的对象可以被重新赋值,锁对象就不受管控了;
//当一个锁被其他对象占有时,当前线程可以对锁对象重新赋值,从而会拿到了运行的权利。
private final String lock1;
private final String lock2;
public DeaThreadTest(String lock1, String lock2){
this.lock1 = lock1;
this.lock2 = lock2;
}
public void run(){
synchronized(lock1){
try {
System.out.println(Thread.currentThread().getName() + "获取了锁:" + lock1);
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(lock2){//拿着lock1寻找lock2
System.out.println("能获取锁:" + lock2 + "嘛?");
}
}
}
public static void main(String[] args) {
new Thread(new DeaThreadTest("lock1", "lock2"), "线程1").start();
new Thread(new DeaThreadTest("lock2", "lock1"), "线程2").start();
}
}
volatile与synchronized对比:
- 原子性、可见行、有序性,volatile只能保证后两个;
- volatile是轻量级的同步机制,只能修饰变量,后者修饰的是方法和代码块;
- 多线程访问下,volatile不会线程阻塞,synchronized会;
- volatile不能保证数据原子性,而后者可以保证数据原子性;
- 虽然synchronized之所以可以保证原子性、可见行、有序性,是因为它只允许一个线程操作,所以单个线程再怎么指令重排也不会影响最终结果。
线程间的通信
等待唤醒机制(wait/notify)
- wait()与notify()都是Object中的方法。
- wait()方发被调用时,线程必须获得该对象级别的锁,即只能在同步方法或代码块中调用wait()。
- wait()调用之后会马上释放锁,给其它线程使用。
- notify()也要在同步方法或代码块中调用。但它不会马上释放锁,而是等当前线程执行完毕再释放。
- notify()唤醒等待队列中的一个线程,notifyAll()方法是唤醒所有线程,直到所用持有该锁的线程都执行完。
- 为了避免虚假唤醒问题,应该将等待机制的判断一直放在在循环中,也即使用while语句,不使用if语句,官网原话(this.wait()、condition.await() 都应该放在while循环判断中,为啥呢?因为用if判断,当下一个时间片轮转到该线程时,该线程的记录点可能已经在if条件判断之后了,故此该次执行会从if语句后开始执行,while则会拉回来重新判断):
wait()与sleep(long)的区别:
- wait()方法等待唤醒,sleep(long)方法自动唤醒,当然了wait(long)也可以自己设置自动唤醒。
- wait()释放锁,sleep()不释放锁。
- wait()是Object类中的方法,要在同步方法或代码块中调用,而sleep(long)是Thread类中的方法,没有使用域的限制。
join()与wait()、sleep(long)的关系
- 如果主线程调用子线程,而主线程早早结束了,它需要使用子线程数据,可子线程的数据还在处理中,这时就可以在主线程中调用子线程的join()方法,作用是等待子线程对象的销毁。
- 当然了可以使用join(long)来设置加入线程的等待时间。
- 在底层中,join()方法其实就是调用的join(long)(令long=0),而join(long)的源码是使用使用wait(long)方法,因此join()方法具有释放锁的特点,这就是与sleep(long)方法的区别之处。
通过管道流进行线程间的通信
(java中提供管道流进行线程间的数据传送,而不用借助于临时文件之类的东西)
- 字节流:PipedInputStream PipedOutputStream
- 字符流:PipedReader PipedWriter
- 其中调用任意一个管道流的connect(xxx)方法对两个线程间的管道流进行连接即可
ThreadLocal类与它的子类InheritableThreadLocal类
- ThreadLocal类主要是给每个线程绑定自己独有的值,具有线程间的隔离性。其中的set(T),get(),remove()等方法可以加入,删除值。
- ThreadLocal类中有一个initialValue()方法底层默认为return null,因此可以自己写一个类,继承ThreadLocal类,重写initialValue()方法,自己设定初始值。
- 使用InheritableThreadLocal类可以实现子线程继承父线程中的值,并可以重写其childValue()方法实现先继承值再修改值。
进阶:Lock接口
- 存在于java.util.concurrent.locks包中
- 并发包中有大量的类使用了Lock接口作为同步的处理方式
- 比synchronized增加了三个高级功能:等待可中断,公平锁,锁可以绑定多个条件。
ReentrantLock类
- ReentrantLock+Condition实例也可以实现线程间的通信,且Condition实例可以有多个,因此可以有选择性的进行线程通知,在调度线程上更加灵活。而notify()方法是由jvm通知随机一个线程的。
- Objectlie类中的wait()对应于Condition类中的await()
- Object中类的wait(long timeout)对应于Condition类中的await(long time, TimeUnit unit)
- Object中类的notify()对应于Condition类中的signal()
- Object中类的notifyAll()对应于Condition类中的signalAll()
- 公平锁与非公平锁
- 公平锁表示线程获取锁的顺序与加锁的顺序相同
- 非公平锁表示线程获取锁的顺序与加锁的顺序无关
Condition 类和 Object 类锁方法区别
1、Condition 类的 awiat 方法和 Object 类的 wait 方法等效;
2、Condition 类的 signal 方法和 Object 类的 notify 方法等效;
3、Condition 类的 signalAll 方法和 Object 类的 notifyAll 方法等效;
4、ReentrantLock 类可以唤醒指定条件的线程,而 object 的唤醒是随机的。
ReentrantLock与synchronized的区别
- synchronized是关键字,属于jvm层面,ReentrantLock是接口、类,属于api层面;
- synchronized不需要手动释放锁,ReentrantLock需要手动unLock(),为了避免程序出现异常而无法正常解锁的情况,使用 ReentrantLock 必须在 finally 控制块中进行解锁操作;
- ReentrantLock可以中断;
- ReentrantLock可以传入参数true可以是公平锁;
- ReentrantLock可以绑定多个条件,就是new多个condition给不同的线程组使用就可以了,实现分组唤醒,精确唤醒。
ReentrantReadWriteLock类
类ReentrantLock具有完全互斥的效果,这样做保证了实例变量的线程安全性,但降低了效率。因此出现了一种读写锁ReentrantReadWriteLock,在某些不需要操作实例变量的方法中,完全可以使用它来提高代码速度。它有两个锁,读操作锁(共享锁)和写操作锁(排它锁)。
- 特点是:两个线程之间读读共享,其它互斥。
并发过程中如何处理原子性,可见性,有序性
- 原子性:并发原子类(并发原子类中的版本号原子引用类还可以解决CAS的ABA问题)、synchronized (当然了 Lock的使用可以代替synchronized)
- 可见性:volatile synchronized final
- 有序性:volatile synchronized
定时器Timer
- Timer类的底层调用了thread.start()方法,开启了一个新的线程。
- Timer类主要功能是设置计划任务,但具体任务交给TimerTask处理。
- TimerTask是一个抽象类,他实现了Runnable接口,因此把任务重写入run()方法即可。