进程:要运行程序时就要把指令加载到CPU,数据加载到内存,当程序被运行,就是开启了一个进程,一个进程包含多个线程,是CPU分配的最小单元,进程通信需要通过网络
线程:线程是进程的最小调度单元,将指令交给CPU来执行
并行:多个线程同时执行
并发:多个线程交替执行
同步:需要等待结果返回,才能继续运行
异步:不需要等待结果返回,就能继续运行
Thread thread = new Thread();
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println();
}
};
new Thread(runnable);
Callable integerCallable = new Callable() {
@Override
public Integer call() throws Exception {
return null;
}
};
new Thread(new FutureTask(integerCallable));
- 创建thread对象
- 实现runnble接口
- 实现callable接口,这个是有返回值的
因为一些原因导致cpu不执行当前线程,而去执行另一个线程比如:
- 线程的cpu时间片用完
- 有更高优先级的线程需要运行
- 线程自己调用了sleep,yield,wait等方法
- 调用sleep方法会让线程从Running进入Timed Waiting状态
- 其他线程可以使用interrupt方法打断正在睡眠的线程,这时sleep方法会抛出InterruptException
- 睡眠结束未必立刻运行
- 调用yield会让当前线程从Running进入Runnable状态,就是让出cpu时间片,下次分配有可能还是这个线程执行
- 具体实现依赖于操作系统的任务调度器
- 谁调用这个线程的join方法,就要等这个线程执行完毕才能继续接着执行
- 实现:
调用join方法时,如果是无参的join,内部就是死循环的wait等待。如果有参就会等待参数的时间退出,会记录当前时间和等待时间,如果虚假唤醒会继续等待相应时间才会退出
- 用来打断阻塞或者正在运行的线程
- 如果打断正在sleep,wait,join等方法时,这几个方法会抛出异常,把被打断的状态改为false
- 打断正在运行的线程,打断状态就是true,被打断以后线程不会停止运行
- 被打断的线程知道自己被打断后可以运行完后边的任务在结束。这种情况称为两阶段终止模式
- park方式是LockSupport的方法,用来让线程睡眠
- 通过interrupt方法可以打断正在park的线程
- 如果打断状态是true,park方法就无法让线程再次睡眠
- park,unpark和wait,notify区别是park以线程为单位来阻塞和休眠,notify是随机唤醒线程,不精准
- park和unpark可以先unpark,而wait和notify不能先notify
- 每个线程都有一个自己的Parker对象,由三部分组成,counter,cond,mutex。线程调用park方法时,检查conunter,如果为0就阻塞,默认为0,然后进入mutex的cond条件中阻塞,设置counter为0,调用unpark时会把counter设置为1,唤醒cond中的线程,设置counter为0。如果先调用unpark方法就会把counter设置为1,在调用park判断counter为1,就不阻塞,继续运行。
synchronized是由Monitor实现的
线程进入被synchronized修饰的代码块时,就会把Mark Word中的信息存成monitor的地址,然后monitor中的owner指向这个线程,别的线程进来以后通过这个对象的Mark Word看看这个monitor的Owner有没有指向别的线程,如果有它就进入到EntryList的阻塞队列中等到别的线程执行完毕。拿到锁的线程执行完毕就会重置Mark Word,唤醒阻塞队列
- 轻量级锁:
开始执行synchronized修饰的方法时,首先在栈帧中创建一个所记录的对象,内部是一个对象指针和用来存储Mark Word,然后会把对象指针指向锁定的对象,然后把Mark Word与锁记录地址和状态替换,如果cas替换成功,就是加锁成功。如果失败分别有两种情况,一是其他线程持有了锁,这是就标识有竞争,锁膨胀。二是自己重入了,那就在添加一个锁记录对象,表示计数,然后cas交换肯定是失败的,然后就在地址哪里存null。解锁时发现地址为null,标识重入了,就直接删除这条锁记录,在退出发现地址有记录,就把Mark Word与对象头里存储的地址替换。如果失败,表示进行了锁膨胀或者升级为了重量级锁,进入重量级锁解锁的流程。 - 锁膨胀:
如果一条线程加轻量级锁时发现已经被别人占用了,就去申请一个Monitor锁,把Mark Word中保存的所记录地址修改为Monitor,然后自己进入EntryList,Owner就指向当前拿锁的线程 - 自旋优化:
重量级锁竞争时,线程会进行自旋拿锁,如果拿锁成功就避免阻塞,也就是非公平性。自旋时自适应的,如果刚刚自旋操作成功过,就多自选几次,反之就少自旋几次。 - 偏向锁:
偏向锁默认延迟生效,第一次会通过CAS将线程ID设置到对象的Mark Word中,之后发现是自己就表示没有竞争,就偏向这个线程。 - 撤销偏向锁:
- 对象调用hashCode方法后,这个对象就不可偏向,因为存hashCode要31位,线程ID要54位,存不下,所以就不可偏向,直接加轻量级锁。
- 当其他线程也来获取偏向锁对象时会升级为轻量级锁
- 调用wait,notify方法
- 批量重偏向:
当撤销偏向锁超过20次后,jvm会重新把这个类的所有对象偏向至加锁线程 - 批量撤销:
当偏向锁撤销超过40次后,jvm会将这个类的所有对象变成不可偏向状态 - 锁消除:
编译器对无法逃逸的局部变量加的锁会进行优化,因为不可能产生竞争,所以会优化掉这个锁
一个java对象在内存里的布局有这几块组成
- 对象头:包含Mark Word和类型指针
- 实例数据
- 对其填充(不满8倍数的字节补够8倍数位字节)
- wait方法会让对象进入monitor的waitSet中等待
- notify方法会让一个随机对象从monitor的waitSet中加入到EntityList中
- 所以这两个方法都必须在同步代码块中才可以执行
- sleep是Threa的方法,wait是Object的方法
- wait需要强制跟synchronized配合使用,sleep不需要
- sleep睡眠不释放锁,wait释放锁
用在一个线程等待另一个线程的执行结果
有一个结果需要从一个线程传递到另一个线程,让他们关联同一个保护对象
优点就是一个线程可以执行完以后随时唤醒另一个线程,join是必须要执行完才会通知另一个线程
- 消费队列可以平衡生产和消费的爱内存资源
- 生产者只负责生产数据,不关心如果消费,消费者只关心怎么消费
- 队列有容量限制,满了就不会再加入,空了不消费
- JDK中的阻塞队列就是这种模式
两个线程抢占两个锁就有可能发生死锁
检测死锁可以使用jconsole工具,或者使用jps定位进程id,再用jstock定位死锁
两个线程同时修改一个变量使其都达不到退出的条件就会一直运行称之为活锁
ReentranLock与synchronized区别是:
- 可中断
- 可以设置超时时间
- 可以设置为公平锁
- 支持多个条件变量
Condition类似于wait的WaitSet,调用await方法时必须获得锁,然后会进入conditionObject中等待,并且释放锁,可以通过signal打断或者超时继续竞争锁。



