线程基础
线程的三种创建方法
继承Thread类.java不支持多继承,子类不能继承其他类
实现runnable接口:还可以继承其他类
实现callable接口:有返回值
线程相关JVM语句
cmder 常用语句记录:
软件:cmder 就是个cmd,就是个命令行,不过她是支持复制粘贴的
jcmd:
发送诊断命令请求到正在运行的Java虚拟机(JVM)。它必须和JVM运行在同一台机器上,并且与启动JVM用户具有相同的组权限。
jcmd 31275 Thread.print -l # 打印线程栈
jcmd 31275 VM.command_line # 打印启动命令及参数
jcmd 31275 GC.heap_dump /data/31275.dump # dump heap
jcmd 31275 GC.class_histogram #查看类的统计信息
jcmd 31275 VM.system_properties #查看系统属性内容
jcmd 31275 VM.uptime #查看虚拟机启动时间
jcmd 31275 PerfCounter.print #查看性能统计
jps -lvm 查看所有在本机上运行java的进程
常用解析:
jstack
jstack -l 9552
打印出所有的线程堆栈
一般第一个就是销毁线程,没有daemon 守护线程标记,是一个用户线程,是等待所有用户线程执行完毕,用来销毁java虚拟机的线程
join()
注意:
join方法为Thread类直接提供的方法,而wait和notify为Object类中的方法
等待线程执行终止之后,继续执行,比如
Thread t1 = new Thread();
t1.start();
t1.join(1000);//毫秒时间
System.out.println("t1's state:"+t1.getState());
就是先将t1线程start,有join,就阻塞主线程
这里是主线程阻塞1秒,过了这个时间就继续执行后面的代码
join()和join(1000)的区别
thread.Join把指定的线程加入到当前线程,可以将两个交替执行的线程合并为顺序执行的线程。
比如在线程B中调用了线程A的Join()方法,直到线程A执行完毕后,才会继续执行线程B。
t.join(); //调用join方法,等待线程t执行完毕
t.join(1000); //等待 t 线程,等待时间是1000毫秒。
源码解析:
/**
* Waits at most {@code millis} milliseconds for this thread to
* die. A timeout of {@code 0} means to wait forever.
*
* <p> This implementation uses a loop of {@code this.wait} calls
* conditioned on {@code this.isAlive}. As a thread terminates the
* {@code this.notifyAll} method is invoked. It is recommended that
* applications not use {@code wait}, {@code notify}, or
* {@code notifyAll} on {@code Thread} instances.
*
* @param millis
* the time to wait in milliseconds
*
* @throws IllegalArgumentException
* if the value of {@code millis} is negative
*
* @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public final synchronized void join(long millis)
throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0);
}
} else {
while (isAlive()) {
long delay = millis - now;
if (delay <= 0) {
break;
}
wait(delay);
now = System.currentTimeMillis() - base;
}
}
}
方法解释:
最多等待 ms 毫秒让该线程死亡。 超时为 0 意味着永远等待。 此实现使用以 this.isAlive 为条件的 this.wait 调用循环。 当线程终止时,调用 this.notifyAll 方法。 建议应用程序不要在 Thread 实例上使用 wait、notify 或 notifyAll。
底层调用isAlive()方法,
Tests if this thread is alive. A thread is alive if it has been started and has not yet died.
Returns:
true if this thread is alive; false otherwise.
测试此线程是否存活。 如果线程已启动且尚未死亡,则该线程处于活动状态。
返回:
如果此线程还活着,则为真; 否则为假。
总结:也就是如果t.join()就和wait类似,主线程要等待这个t线程结束才能执行下去,底层调用object的wait()方法
join(xxx)表示最多等待xxx毫秒,如果在这个时间内线程执行完毕,则立即执行join后面的代码。
sleep()
不释放锁——原子性
Thread类中的一个静态方法,暂时让出执行权,不参与CPU调度,但是。时间到了就进入到就绪状态,一旦获取到CPU时间片,则继续执行。
是一个native static方法
可以用中断异常
t1.interrupt();
yield()—假装客气
不释放锁——原子性
知识点:Thread类中的静态native方法;让出剩余的时间片,本身进入就绪状态,CPU再次调度还可能调度到本线程。
易混淆知识点:sleep是在一段时间内进入阻塞状态,cpu不会调度它。而yield是让出执行权,本身还处于就绪状态,cpu还可能立即调度它,可以不理会它
wait()/notify-object
释放锁
notify不释放锁
有且仅有wait会释放锁
wait一般与synchronize+notify连用,因为会释放锁
interrupt()
线程中断
知识点:线程中断是线程间的一种协作模式,通过设置线程中断标志来实现,线程根据这个标志来自行处理。
public void interrupt() 中断标志设置为true
public boolean isInterrupted() 获取中断标记
public static boolean interrupted() 将中断标记取反,会改变当前线程的中断标记状态
获取当前线程的状态:容易混淆!,在主函数中就是获取main线程
会导致sleep和join线程抛出中断异常
线程安全问题原理
可见性和原子性
1.线程安全的概念:当多个线程访问某一个类、对象或方法时,这个类、对象或方法都能表现出与单
线程执行时一致的行为,那么这个类、对象或方法就是线程安全的。
2.线程安全问题都是由全局变量及静态变量引起的。
3.若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程
安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。
synchronized关键字
原理就是保证原子性,维护可见性,维护秩序。
Synchronized的作用是加锁,所有的synchronized方法都会顺序执行,(这里只占用CPU的顺序)。
Synchronized方法执行方式:
– 首先尝试获得锁
– 如果获得锁,则执行Synchronized的方法体内容。
– 如果无法获得锁则等待,并且不断的尝试去获得锁,一旦锁被释放,则多个线程会同时去尝
试获得所,造成锁竞争问题。
锁竞争问题,在高并发、线程数量高时会引起CPU占用居高不下,或者直接宕机。
对象锁和类锁--区别
Synchronized作用在非静态方法上代表的对象锁,一个对象一个锁,多个对象之间不会发生锁竞争。
Synchronized作用在静态方法上则升级为类锁,所有对象共享一把锁,存在锁竞争。
比如
User user1 = new User();
User user2 = new User();
synchronized(use1)
synchronized(use2)
锁是不同的对象,不同对象之间吧刽发生锁竞争
但是如果 static synchronize 是类锁的话就会发生竞争
原理是类锁的话,静态变量和静态方法是存储在方法区中的,访问的是同一个数据
但是对象锁而言,对象是存储在堆中的,比如上述的user1和user2都在堆中是不同的对象,不是同一个资源,所以不存在锁竞争
一个类里面如果方法上有synchronize表示当前的锁是当前类对象
对象锁的同步和异步
同步:必须等待方法执行完毕,才能向下执行,共享资源访问的时候,为了保证线程安全,必须同步。
异步:不用等待其他方法执行完毕,即可立即执行,例如Ajax异步。
对象锁只针对synchronized修饰的方法生效、 对象中的所有synchronized方法都会同步执行、而非
synchronized方法异步执行
避免误区:类中有两个synchronized方法,两个线程分别调用两个方法,相互之间也需要竞争锁,
因为两个方法从属于一个对象,而我们是在对象上加锁
脏读问题
多个线程访问同一个资源,在一个线程修改数据的过程中,有另外的线程来读取数据,就会引起脏
读的产生。
为了避免脏读我们一定要保证数据修改操作的原子性、并且对读取操作也要进行同步控制
锁重入
同一个线程得到了一个对象的锁之后,再次请求此对象时可以再次获得该对象的锁。
父子类可以锁重入
public class DemoThread05{
public synchronized void run1(){
System.out.println(Thread.currentThread().getName()+">run1...");
//调用同类中的synchronized方法不会引起死锁
run2();
}
public synchronized void run2(){
System.out.println(Thread.currentThread().getName()+">run2...");
}
public static void main(String[] args) {
final DemoThread05 demoThread05 = new DemoThread05();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
demoThread05.run1();
}
});
thread.start();
}
}
抛异常和锁的关系
一个线程在获得锁之后执行操作,发生错误抛出异常,则自动释放锁
1、可以利用抛出异常,主动释放锁
2、程序异常时防止资源被死锁、无法释放
3、异常释放锁可能导致数据不一致
synchronized代码块
可以达到更细粒度的控制
当前对象锁
类锁
任意对象锁
总结:
同类型锁之间互斥,不同类型的锁之间互不干扰
不要在线程内修改对象锁的引用
不能修改锁的指向地址。如果是对象,修改对象里的值是不会引起锁失效的
并发和死锁
是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现
象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永
远在互相等待的进程称为死锁进程。
线程间的通讯
每个线程都是独立运行的个体,线程通讯能让多个线程之间协同工作
Object类中的wait/notify方法可以 实现线程间通讯
Wait/notify必须与synchronized一同使用
Wait释放锁、notify不释放锁
notify与notifyAll的区别
notify本身不释放锁
Notify只会通知一个wait中的线程,并把锁给他,不会产生锁竞争问题,但是该线程处理完毕之后
必须再次notify或notifyAll,完成类似链式的操作。
NotifyAll会通知所有wait中的线程,会产生锁竞争问题。
notify的通知依赖底层JVM的实现,这里notify依赖的是native方法,找到源码,目前是先进先出,就是最早wait()的先被唤醒。
synchronized的wait和notify是位于ObjectMonitor.cpp中
这里实际上是将_WaitSet中的第一个元素进行出队操作
原来hotspot对notofy()的实现并不是我们以为的随机唤醒, 而是“先进先出”的顺序唤醒!`
实战:实现阻塞式线程安全队列
使用synchronized、wait、notify实现带阻塞的线程安全队列
功能描述:
功能1:在队列元素满的时候,put阻塞,在队列元素空的时候,get阻塞
功能2 :线程安全
代码:DemoThread20
视频中的代码 仍然有bug,不过我已经改正了
while (this.list.size() == this.maxSize) {
lock.wait();
}
问题就是wait中的线程被唤醒时和新线程竞争的时候破坏了数据的原子性,因为集合是非原子性的
守护线程和用户线程
线程分类:daemon线程(守护线程)、user线程(用户线程)
易混淆知识点:main函数所在的线程就是一个用户线程
重要知识点1:最后一个user线程结束时,JVM会正常退出,不管是否有守护线程正在运行。反过来说:只要有一个用户线程还没结束,JVM进程就不会结束。
重要知识点2:父线程结束后,子线程还可以继续存活,子线程的生命周期不受父线程的影响
shutdown和kill的区别
shutdown会将所有正在执行的请求执行完毕,而kill不会,尤其是kill -9
线程上下文切换
CPU采用时间片轮巡的策略,线程切换的过程就是上下文切换
当前线程使用完时间片后就会进入就绪状态,让出CPU执行权给其他线程,此时就是从当前线程的上下文切换到了其他线程。
当发生上下文切换的时候需要保存执行现场,待下次执行时进行恢复。
所以频繁的、大量的上下文切换会造成一定资源开销
验证
shutdown会将所有controller正在执行的请求执行完毕,而kill不会