Java多线程学习
线程的调度
- 抢占式(重点)
在抢占模式下,操作系统负责分配CPU时间给各个进程,一旦当前的进程使用完分配给自己的CPU时间,操作系统将决定下一个占用CPU时间的是哪一个线程。 - 协作式
协作式线程调度器在将cpu控制权交给其他线程钱,会等待正在运行的线程自己去暂停,然后才可以交给另外一个线程。一些早期或者特殊用途的虚拟机 可能会使用这种方式。
进程与线程
进程的运行需要较多的资源,因此,操作系统能够同时运行的进程数量是有限的。进程间的切换与通信也存在较大的开销。为了能够并行的执行更多的任务,提升系统的效率,才引入了线程,线程间的切换开销比进程间的切换开销小得多。因此线程是运行的基本单元(CPU调度的基本单位)、进程是资源分配的基本单元。
线程是进程的一部分,一个进程拥有1~~N个线程,线程共享所在进程的资源。
线程分为守护线程和用户线程。
- 守护线程:做一些辅助操作,程序不会等待守护线程结束而结束。
thread.setDaemon(true)
将一个线程设置为守护线程必须在thread.start()之前设置。
在Daemon线程中产生的新线程也是Daemon的。
不是所有的应用都可以分配给Daemon线程来进行服务的,比如读写操作或者计算逻辑。 - 用户现场:用户线程结束,程序也就结束了,即使守护线程没有终止也会随之停止。
线程的生命周期
- 新建:线程被new出来的状态。
- 就绪:使用start开启线程,此时线程不一定会执行,处于竞争资源的时候。
- 运行:获取到了运行资源,执行代码。
- 阻塞:不执行代码,暂时放弃CPU的使用权,有可能会释放锁,进行等待,直到满足某个条件时回到就绪状态。
- 死亡:线程当中代码执行完毕或者执行过程中出现异常,或被强行中断(不建议)。
线程的创建
继承Thread类
通过继承Thread类,覆盖其run()方法即可编写一个线程。
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}
}
new MyThread().start();
让线程处于就绪状态。
实现Runable接口
Java单继承的特性并不满足开发需求所以可以通过实现Runable接口来创建线程。
public class MyThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}
}
new Thread(new Mythread()).start();
让线程处于就绪状态。
Thread与Runable联系
在Runable接口源码中其实只定义了一个抽象的run方法。
Runable源码。
@FunctionalInterface
public interface Runnable {
/**
* When an object implementing interface <code>Runnable</code> is used
* to create a thread, starting the thread causes the object's
* <code>run</code> method to be called in that separately executing
* thread.
* <p>
* The general contract of the method <code>run</code> is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}
Thread实现了Runable并对其功能进行了扩展,扩展的功能主要还是通过JNI调取的由C++/C写的代码。
Thread类部分源码。
public
class Thread implements Runnable {
@Override
public void run() {
if (target != null) {
target.run();
}
}
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
public synchronized void start() {
/**
* This method is not invoked for the main method thread or "system"
* group threads created/set up by the VM. Any new functionality added
* to this method in the future may have to also be added to the VM.
*
* A zero status value corresponds to state "NEW".
*/
if (threadStatus != 0)
throw new IllegalThreadStateException();
/* Notify the group that this thread is about to be started
* so that it can be added to the group's list of threads
* and the group's unstarted count can be decremented. */
group.add(this);
boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
/* do nothing. If start0 threw a Throwable then
it will be passed up the call stack */
}
}
}
······
······
······
······
······
······
}
Callable与Future
Java5添加了Callable接口和Future接口,使得线程可以产生返回值,并且可以抛出异常。
public class MyThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
sleep(3000);
return 10;
}
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
Future<Integer>f=executor.submit(new MyThread());
try {
System.out.println(f.get()); //执行到此处会被阻塞,直到获取到返回值。
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
executor.shutdown();
}
}
常用方法介绍
方法 | 作用 |
---|---|
t.start(); | 启动线程t |
t.checkAccess(); | 检查当前线程是否有权限访问线程t |
t.interrupt(); | 尝试通知线程t中断 |
Thread.currentThread().isInterrupted() | 检查当前线程是否被要求中断 |
t.setPriority(int) | 设置线程的优先级,1~10依次增大 |
t.isDaemon(); | 判断线程t是否为守护线程 |
t.setDaemon(boolean) | 在start前设置线程t是否为守护线程,默认false |
t.isAlive(); | 判断线程t是否存活 |
t.join(long); | 等待线程t终止在执行 ,参数为超时时间 |
t.setName(String); | 设置线程名称 |
yield(); | 让当前线程让出CPU,转为就绪状态,重新争夺资源,当只有大于等于t优先级的线程才能被执行。 |
Thread.currentThread(); | 得到当前线程对象的引用 |
Thread.sleep(long) | 让当前线程转为阻塞状态,参数表示阻塞时间,后转为就绪状态,不会释放锁 |
wait(long); | Object当中的方法,和sleep类似,但是会释放锁,所以只能在synchronized作用域中使用 |
t.notify(); | wait()的对应方法,可以唤醒线程t,同样在synchronized作用域中使用 |
线程安全与效率
Java多线程的内存模型
我们声明的对象的成员变量、静态变量等非局部变量都位于主内存中,而每个线程又都有自己的工作内存,工作内存是CPU中寄存器与高速缓存的抽象描述,并不是真正的一块内存,一般情况下,当主线程获得CPU使用权后,开始执行前会将主内存当中的变量load到工作内存,处理完成在save会主内存。
可能产生的问题
线程安全主要考虑的三个方面
- 可见性:一个线程对某个变量修改时,其他线程可以立即检测到。
- 有序性:禁止指令重排。
- 原子性:线程在执行某一连续操作时要么都执行,要么都不执行。
线程同步的机制(四个)
- 临界区:在同一时刻只允许一个线程执行的代码块称为临界区,临界区通常用锁机制来实现。
- 互斥量:互斥量跟临界区很相似,只有拥有互斥对象的线程才具有访问资源的权限,由于互斥对象只有一个,因此就决定了任何情况下此共享资源都不会同时被多个线程所访问。
- 信号量:信号量允许有限数量的线程在同一时刻访问同一资源。
- 事件:用来通知线程有一些事件已发生,从而启动后继任务的开始。
可能产生的问题
- 饥饿:某个线程长时间得不到所需要的资源,而总是经常性的处于就绪状态。
- 死锁:两个或两个以上线程在资源竞争中互不相让,形成资源等待或长时间处于阻塞状态。
如何确保线程安全
volatile关键字
作用:禁止指令重排和保证其可见性,但是不能解决原子性。
原理: 被volatile关键字修饰的变量会存在一个“lock:”的前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能,它的作用是将本处理器的缓存写入了内存,该写入动作也会引起别的处理器或者别的内核无效化(Invalidate,MESI协议的I状态)其缓存,这种操作相当于对工作内存与主内存中的变量做了一次“store和write”操作。所以通过这样一个操作,可让前面volatile变量的修改对其他处理器立即可见。lock指令的更底层实现:如果支持缓存行会加缓存锁(MESI);如果不支持缓存锁,会加总线锁。
CAS操作
更新某个变量前,检查变量的当前值是否符合期望值,如果相符就使用新值替换当前值,否则自旋尝试重新获取,直到成功。
可能存在的问题:
- ABA问题:如果一个值原来是A,接着变成了B,之后又变成了A,执行CAS时会发现值没有预期当中的变化不执行。
- 循环开销:当冲突严重时,自旋的开销严重增加CPU负担。
- 只能保证一个共享变量的原子性。
synchronized关键字
是Java的关键字,被用于标记一个方法或代码块,通过给对象上锁的方式,将自己的作用域变为一个临界区,由编译器负责加锁与解锁的操作,是自动进行的。
synchronized | 悲观锁、偏向锁—>轻量级锁—>重量级锁(逐渐升级)、非公平锁、可重入锁、独占锁 |
作用范围:
- 修饰代码块:
synchronized(data){
······
······
}
作用范围是{}括起来的代码块,作用对象是()内的对象,表示获得该对象的锁。
- 修饰非静态方法:
public synchronized void set(){
······
······
}
被修饰的方法称为同步方法,其作用范围是整个方法,作用的对象是拥有这个方法的对象,执行此方法时会先获取调用此方法的对象的锁。
注意:在定义接口方法时不能使用synchronized来修饰。构造方法也不能用synchronized来修饰,但是构造方法体内可以使用synchronized来修饰。
- 修饰静态方法:
public synchronized static void set(){
······
······
}
其作用范围是整个静态方法,作用对象是该静态方法所在的类(静态方法属于类不属于对象),锁定的是该类本身。
- 修饰一个类:
synchronized(MyThread.class){
······
······
}
作用范围是{}括起来的部分,和修饰静态方法一样,获取的是类本身的锁。
Lock接口
与synchronized类似,对需要的对象添加锁,与synchronized不同,需要unlock主动去解锁,最好放在finally块当中。
具体实现类:
实现类 | 作用 |
---|---|
ReentrantLock() | 可重入锁 |
ReentrantReadWriteLock() | 读写锁 |
readLock() | 读锁(在ReentrantReadWriteLock类当中获取) |
writeLock() | 写锁(在ReentrantReadWriteLock类当中获取) |
各种概念锁
锁 | 解释 |
---|---|
乐观锁 | 总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据(CAS操作)。 |
悲观锁 | 总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁 |
自旋锁 | 线程循环地去获取锁 |
非自旋锁/互斥锁 | 普通锁。获取不到锁,线程就进入阻塞状态。 |
适应性自旋锁 | 自旋一定次数或时间获取不到锁变为非自旋锁 |
偏向锁 | 无实际竞争,且将来只有第一个申请锁的线程会使用锁。 |
轻量级锁 | 无实际竞争,多个线程交替使用锁;允许短时间的锁竞争。可以优化为自旋形式 |
重量级锁 | 有实际竞争,且锁竞争时间长。一般为互斥形式 |
公平锁 | 指在分配锁前检查是否有线程在排队等待获取该锁,优先将锁分配给排队时间最长的线程(先来先服务) |
非公平锁 | 指在分配锁时不考虑线程排队等待的情况,直接尝试获取锁,在获取不到锁时再排到队尾等待(谁抢到是谁的) |
可重入锁/递归锁 | 即若当前线程执行某个方法已经获取了该锁,那么在该方法或其调用的方法中尝试再次获取锁时,可以获取到 |
不可重入锁 | 即若当前线程执行某个方法已经获取了该锁,那么在该方法或其调用的方法中尝试再次获取锁时,就会获取不到被阻塞。 |
排他锁/写锁/独占锁 | 只能被一个线程读取和修改,其他线程获取时会被阻塞 |
共享锁/读锁 | 允许多个线程读取,但不能修改 |