一、基本概念
1、线程和进程
进程:进程是程序的一次执行过程,是系统运行程序的基本单位,每一个进程都有自己独立的内存空间。
线程:线程是一个比进程更小的执行单位,属于进程当中的一个执行单元,负责进程当中程序的执行,一个进程当中可以存在多个线程。
区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。
2、并发与并行
并发:指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。
并行:同一时刻同时有多条指令在多个处理器上同时执行。
并行在多处理器系统中存在,而并发可以在单处理器和多处理器系统中都存在。
3、synchronized
3.1 修饰对象
synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
1. 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象;
2. 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象(实例);
3. 修改一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象(类对象);
3.2 内部状态
ContentionList:锁竞争队列,所有请求锁的线程都被放在竞争队列中。
EntryList:竞争候选列表,在锁竞争队列中有资格成为候选者来竞争锁资源的线程被移动到候选列表中。
WaitSet:等待集合,调用wait方法后阻塞的线程将被放在WaitSet。
OnDeck:竞争候选者,在同一时刻最多只有一个线程在竞争锁资源,该线程的状态被称为OnDeck。
Owner:竞争到锁资源的线程状态。
!Owner:释放锁后的状态。
3.3 实现原理
1.收到新的锁请求时首先自旋,如果通过自旋也没有获取锁资源,被放入ContentionList。
2.为了防止ContentionList尾部的元素被大量线程进行CAS(并交换)访问影响性能,Owner线程会在是释放锁时将ContentionList的部分线程移动到EntryList并指定某个线程(一般是最先进入的)为OnDeck线程。Owner并没有将锁直接传递给OnDeck线程而是把锁竞争的权利交给他,该行为叫做竞争切换,牺牲了公平性但提高了性能。
3.获取到锁的OnDeck线程会变为Owner线程,未获取到的仍停留在EntryList中Owner线程在被wait阻塞后会进入WaitSet,直到某个时刻被唤醒再次进入EntryList。
ContentionList、EntryList、WaitSet中的线程均为阻塞状态
4.当Owner线程执行完毕后会释放锁资源并变为!Owner状态。
3.4 锁状态
在 jdk1.6 之前,synchronized 被称为重量级锁,在 jdk1.6 中,为了减少获得锁和释放锁带来的性能开销,引入了偏向锁和轻量级锁。
无锁:没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。
偏向锁:偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要同步。目的:减少只有一个线程执行同步代码块时的性能消耗,即在没有其他线程竞争的情况下,一个线程获得了锁。
轻量级锁:当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞。目的:在多线程交替执行同步代码块时(未发生竞争),避免使用互斥量(重量锁)带来的性能消耗。但多个线程同时进入临界区(发生竞争)则会使得轻量级锁膨胀为重量级锁。
重量级锁:其他线程试图获取锁时,都会被阻塞,只有持有锁的线程释放锁之后才会唤醒这些线程。
3.5 锁升级
偏向锁升级:当一个线程访问同步块并获取锁时,会在对象头和栈帧中记录存储锁偏向的线程ID,以后该线程在进入同步块时先判断对象头的Mark Word里是否存储着指向当前线程的偏向锁,如果存在就直接获取锁。
轻量级锁:当其他线程尝试竞争偏向锁时,锁升级为轻量级锁(线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间(Lock Record),并将对象头中的MarkWord替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,标识其他线程竞争锁,当前线程便尝试使用自旋来获取锁)。
重量级锁:锁在原地循环等待的时候,是会消耗CPU资源的。所以自旋必须要有一定的条件控制,否则如果一个线程执行同步代码块的时间很长,那么等待锁的线程会不断的循环反而会消耗CPU资源。默认情况下锁自旋的次数是10 次,可以使用-XX:PreBlockSpin参数来设置自旋锁等待的次数。10次后如果还没获取锁,则升级为重量级锁。
二、多线程
1、实现方式
1.1 继承Thread类
public class MyThread extends Thread{
public MyThread(String name) {
super(name);
}
@Override
public void run(){
for (int i = 0; i < 10; i++) {
System.out.println(“我开始学习了”)
System.out.println(getName()+" : "+i);
}
}
}
public class Test {
public void main(String[] args) {
MyThread mt = new MyThread("新的线程!");
mt.start();
for (int i = 0; i < 10; i++) {
System.out.println("main线程!"+i);
}
}
}
1.2 实现Runnable接口
public class MyRunable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName() + "Runnable线程:" + i);
}
}
}
public class Test {
public static void main(String[] args) {
//创建自定义类对象 线程任务对象
MyRunable mr = new MyRunable();
//创建线程对象
Thread t = new Thread(mr, "支线");
t.start();
for (int i = 0; i < 20; i++) {
System.out.println("主线 " + i);
}
}
1.3 实现 Callable接口
public class MyCallable implements Callable {
@Override
public Object call() throws Exception {
for (int i = 0; i < 20; i++) {
System.out.println("callable线程代码执行:"+i);
}
return "执行完成一次";
}
}
public class Test {
public static void main(String[] args) {
//创建自定义类对象 线程任务对象
MyCallable callable=new MyCallable();
FutureTask task=new FutureTask(callable);//接收Callable的实例
new Thread(task).start();
for (int i = 0; i < 20; i++) {
System.out.println("主线程代码执行:"+i);
}
try {
Object o = task.get();
System.out.println(o);
} catch (Exception e) {
}
}
}
1.4 区别
run()方法和start()方法的区别:
1.start用于启动线程;run用于执行线程的运行时代码。
2.一个线程对线的 start() 方法只能调用一次,多次调用会抛出 java.lang. IllegalThreadState Exception异常;run() 方法没有限制。
3.run方法并不是多线程,它只是线程里面的一个函数,直接调用只是调用了一个函数。
runnable()和callable的区别:
Callable接口call方法有返回值,调用FutureTask.get()得到,此方法会塞阻主进程的继续往下执行,如果不调用不会阻塞。
Runnable接口run方法无返回值。
2、 线程内部状态方法
2.1 线程的状态
线程共包括以下 5 种状态:
1. 新建状态(New): 线程对象被创建后,就进入了新建状态。例如,Thread thread = new Thread()。
2. 就绪状态(Runnable): 也被称为“可执行状态”。线程对象被创建后,其它线程调用了该对象的start()方法,从而来启动该线程。例如,thread.start()。处于就绪状态的线程,随时可能被CPU调度执行。
3. 运行状态(Running): 线程获取CPU权限进行执行。需要注意的是,线程只能从就绪状态进入到运行状态。
4. 阻塞状态(Blocked): 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:
1) 等待阻塞 -- 通过调用线程的wait()方法,让线程等待某工作的完成。
2) 同步阻塞 -- 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态。
3) 其他阻塞 -- 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
5. 死亡状态(Dead): 线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
2.2 线程的调度方法
-
start():启动线程,调用此线程的run方法.
-
wait():使一个线程处于等待(阻塞)状态,并释放所持有对象的锁。
-
yield():当前线程暂停一下,让系统的线程调度器重新调度一次,希望优先级与当前线程相同或更高的其他线程能够获得执行机会。但是会出现某个线程调用了yield方法暂停之后,线程调度器又将其调度出来重新执行。
-
join():等待该线程终止。
-
sleep():使一个正在运行的线程处于睡眠状态,不会放弃锁。需处理异常。
-
notify():唤醒等待的线程,由JVM决定唤醒哪个线程,且与优先级无关。
-
notifyAll():唤醒所有处于等待状态的线程,同时只有获得锁的线程才能进入就绪状态。
-
stop():强迫线程停止执行,是一个已经过时的方法。