1. java线程实现/创建的方式(主要是两种)
1.继承Thread类
本质是通过实现Runnable接口的一个实例,代表一个线程的实例
public class MyThread extends Thread{
public void run(){
System.out.println("run()")
}
}
MyThread thread = new MyThread();
thread.start();
2.实现Runnable接口
public class MyThread extends OtherClass implements Runnable{
public void run(){
System.out.println("run()")
}
}
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
2. 线程的生命周期
新建->就绪->运行->阻塞->死亡
1.新建:程序使用new创建线程后,就是新建状态,jvm会为他分配内存,并初始化成员变量的值
2.就绪:线程对象调用start()后,就是就绪状态。java虚拟机会创建方法调用栈和程序计数器,等待调度运行
3.运行:处于就绪状态的线程获得了cpu,执行run()的线程执行体,就是运行状态
补充:线程什么情况会从运行状态变成阻塞状态
- 使用sleep方法,会主动放弃所占用的系统资源
- 线程调用一个阻塞式io方法,在方法返回之前都是阻塞状态
- 线程试图获得一个同步监视器,但更改同步监视器正被其他线程所持有
- 线程等待某个通知notify
- 程序调用线程的suspend方法会被线程挂起,容易造成死锁不建议使用
4.阻塞:线程因为某种原因放弃了对cpu的使用权,即让出了cpu timeslice,暂时停止运行,直到线程进入可运行(runnable)状态,才有机会再次获得cpu timeslice转到运行状态
等待阻塞、同步阻塞、其他阻塞
5.死亡:
- 正常结束:run()或call()方法执行完成,线程正常结束
- 异常结束:线程抛出一个未捕获的Exception或Error
- 调用stop:直接调用该线程的stop()方法来结束线程,容易导致死锁
3. 终止线程的方式
- 正常运行结束
- 使用退出标志退出线程(volatile关键字的目的是让exit同步)
- interrupt方法结束线程
- stop方法终止线程(线程不安全)
4. sleep和wait区别
- sleep()是静态的Thread类的方法,wait()是Object类中的方法
- sleep()方法导致线程暂停执行指定的时间,让出cpu给其他线程,但是他的监控状态依旧保持,到了指定时间又会自动恢复运行状态,在调用的过程中,线程不会释放对象锁
- 调用wait()方法过程中,线程会放弃对象锁,进入等待对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态
5.start和run区别
- start()用来启动线程,不需要等待run()方法体代码执行完就可以继续执行下面的代码。
- run()称为线程体。包含执行线程的内容,线程进入到了运行状态,开始运行run中的代码,运行结束后,线程才会终止,然后cpu再调度其他线程
6. 乐观锁、悲观锁和自旋锁
6.1 乐观锁(AtomicInteger)
乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新(说明没有人去修改过数据,所以更新),不一样算提交失败),如果失败则要重复读-比较-写的操作。java 中的乐观锁基本都是通过 CAS 操作实现的,CAS 是一种更新的原子操作,比较当前值跟传入值是否一样,一样则更新,否则失败。
6.2 悲观锁 ( synchronize lock)
悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会 block 直到拿到锁。java中的悲观锁就是Synchronized,AQS框架下的锁则是先尝试cas乐观锁去获取锁,获取不到,才会转换为悲观锁,如 RetreenLock。
6.3 自旋锁
如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。
出现的原因:如果存在大量互斥同步代码,当出现高并发的时候,系统内核态就需要不断地去挂起线程和恢复线程,频繁的操作就会对系统的并发性能有一定的影响
java 非公平自旋锁
/**
* 锁的持有者
*/
private AtomicReference<Thread> owner = new AtomicReference<>();
/**
* 记录锁重入次数
*/
private volatile int count = 0;
//volatile对其他线程的可见性
public void lock(){
Thread thread = Thread.currentThread();
if (thread == owner.get()){
//获取AtomicReference的当前对象引用值。
count ++;
return;
}
/**
原子性地更新AtomicReference内部的value值,
其中expect代表当前AtomicReference的value值,update则是需要设置的新引用值。
该方法会返回一个boolean的结果,
当expect和AtomicReference的当前值不相等时,修改会失败,返回值为false,
若修改成功则会返回true。
**/
while (!owner.compareAndSet(null,thread));
}
public void unlock(){
Thread thread = Thread.currentThread();
if (thread == owner.get()){
if (count > 0) count --; //锁重入, 直接自减即可
else owner.set(null);//设置AtomicReference最新的对象引用值,该新值的更新对其他线程立即可见。
}
}
public static void main(String[] args) {
TestController test = new TestController();
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始尝试获取自旋锁");
test.lock();
try {
System.out.println(Thread.currentThread().getName() + "获取到了自旋锁");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
test.unlock();
System.out.println(Thread.currentThread().getName() + "释放了了自旋锁");
}
}
};
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
Thread t3 = new Thread(runnable);
t1.start();
t2.start();
t3.start();
}
java 公平锁自旋锁
/**
* 当前持有锁的号码
*/
private AtomicInteger serviceNum = new AtomicInteger(0);
/**
* 记录锁重入次数
*/
private volatile int count = 0;//volatile对其他线程的可见性
/**
* 排队号码
*/
private AtomicInteger ticketNum = new AtomicInteger(0);
/**
* 各线程存放自己所申请的排队号码
*/
private static ThreadLocal<Integer> threadLocalNum = new ThreadLocal<>();
public void lock(){
Integer num = threadLocalNum.get();
if (num != null && num == serviceNum.get()){
//当前线程已经持有锁, 则记录重入次数即可
count ++;
return;
}
//申请一个排队号码
num = requestTickerNum();
System.out.println("申请一个排队号码:"+num);
threadLocalNum.set(num);
//自旋等待,直到该排队号码与serviceNum相等
while (num != this.serviceNum.get());
}
private Integer requestTickerNum() {
return ticketNum.getAndIncrement();
}
public void unlock(){
Integer num = threadLocalNum.get();
if (num != null && num == serviceNum.get()){
if (count > 0) count--;//锁重入, 直接自减即可
else{
threadLocalNum.remove();
//自增serviceNum, 以便下一个排队号码的线程能够退出自旋
serviceNum.set(num + 1);
}
}
}
public static void main(String[] args) {
TestController test = new TestController();
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "开始尝试获取自旋锁");
test.lock();
try {
System.out.println(Thread.currentThread().getName() + "获取到了自旋锁");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
test.unlock();
System.out.println(Thread.currentThread().getName() + "释放了了自旋锁");
}
}
};
Thread t1 = new Thread(runnable);
Thread t2 = new Thread(runnable);
Thread t3 = new Thread(runnable);
Thread t4 = new Thread(runnable);
t1.start();
t2.start();
t3.start();
t4.start();
}
7. 同步锁和死锁
7.1 同步锁
当多个线程同时访问同一个数据时,容易出现问题,为了避免,要保证线程同步互斥(指并发执行的多个线程),在同一时间内值允许一个线程访问共享数据。使用synchronized来获取一个对象的同步锁
7.2 死锁
多个线程同时被阻塞,他们中的一个或者全部都在等待某个资源被释放
产生死锁的条件:(下面同时存在才是死锁)
- 互斥条件:资源不能被共享,只能被同一个进程使用
- 请求与保持条件:已经得到的资源进程可以申请新的资源
- 非剥夺条件:已经分配的资源不能从相应的进程中被强制剥夺
- 循环等待条件:系统中若干进程组成环路,该环路中每个进程都在等待相邻进程占用的资源
常见死锁例子:进程a中包含资源a,进程b中包含资源b,a的下一步需要资源b,b的下一步需要资源a,所以他们就互相等待对方占有的资源释放,所以产生一个循环等待死锁
解决方法:
- 不管他,无视他
- 检测死锁并恢复
- 资源进行动态分配
- 破除上面的死锁条件
产生死锁:
情况一:
/**
* 一个简单的死锁类
* t1先运行,这个时候flag==true,先锁定obj1,然后睡眠1秒钟
* 而t1在睡眠的时候,另一个线程t2启动,flag==false,先锁定obj2,然后也睡眠1秒钟
* t1睡眠结束后需要锁定obj2才能继续执行,而此时obj2已被t2锁定
* t2睡眠结束后需要锁定obj1才能继续执行,而此时obj1已被t1锁定
* t1、t2相互等待,都需要得到对方锁定的资源才能继续执行,从而死锁。
*/
public class DeadLock implements Runnable{
private static Object obj1 = new Object();
private static Object obj2 = new Object();
private boolean flag;
public DeadLock(boolean flag){
this.flag = flag;
}
@Override
public void run(){
System.out.println(Thread.currentThread().getName() + "运行");
if(flag){
synchronized(obj1){
System.out.println(Thread.currentThread().getName() + "已经锁住obj1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(obj2){
// 执行不到这里
System.out.println("1秒钟后,"+Thread.currentThread().getName()
+ "锁住obj2");
}
}
}else{
synchronized(obj2){
System.out.println(Thread.currentThread().getName() + "已经锁住obj2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(obj1){
// 执行不到这里
System.out.println("1秒钟后,"+Thread.currentThread().getName()
+ "锁住obj1");
}
}
}
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new DeadLock(true), "线程1");
Thread t2 = new Thread(new DeadLock(false), "线程2");
t1.start();
t2.start();
}
情况二:
public class SyncThread implements Runnable{
private Object obj1;
private Object obj2;
public SyncThread(Object o1, Object o2){
this.obj1=o1;
this.obj2=o2;
}
@Override
public void run() {
String name = Thread.currentThread().getName();
synchronized (obj1) {
System.out.println(name + " acquired lock on "+obj1);
work();
synchronized (obj2) {
System.out.println("After, "+name + " acquired lock on "+obj2);
work();
}
System.out.println(name + " released lock on "+obj2);
}
System.out.println(name + " released lock on "+obj1);
System.out.println(name + " finished execution.");
}
private void work() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws InterruptedException {
Object obj1 = new Object();
Object obj2 = new Object();
Object obj3 = new Object();
Thread t1 = new Thread(new SyncThread(obj1, obj2), "t1");
Thread t2 = new Thread(new SyncThread(obj2, obj3), "t2");
Thread t3 = new Thread(new SyncThread(obj3, obj1), "t3");
t1.start();
Thread.sleep(1000);
t2.start();
Thread.sleep(1000);
t3.start();
}
8. 线程管理
8.1 线程睡眠(sleep)
让当前正在执行的线程暂停一段时间,并进入阻塞状态(使用Thread.sleep()方法)
当睡眠结束后,重新进入到就绪状态。
public static void main(String[] args) throws InterruptedException {
MyThread thread = new MyThread();
thread.start();
thread.sleep(1000);//睡眠的是mian线程,不是MyThread 线程
Thread.sleep(10);
for(int i=0;i<100;i++){
System.out.println("main"+i);
}
}
8.2 线程让步(yield)
让当前正在执行的线程暂停,让出cpu资源给其他线程。yield后进入就绪状态。(可能出现:线程调度器又将其调度出来重新进入到运行状态运行)
当调用yield()方法暂停后,优先级比当前线程相同或者高时,就绪状态的线程更有可能获得执行机会
public class Test1 {
public static void main(String[] args) throws InterruptedException {
new MyThread("低级", 1).start();
new MyThread("中级", 5).start();
new MyThread("高级", 10).start();
}
}
class MyThread extends Thread {
public MyThread(String name, int pro) {
super(name);// 设置线程的名称
this.setPriority(pro);// 设置优先级
}
@Override
public void run() {
for (int i = 0; i < 30; i++) {
System.out.println(this.getName() + "线程第" + i + "次执行!");
if (i % 5 == 0)
Thread.yield();
}
}
}
运行结果:先把高级的执行完再执行中级最后执行低级
sleep()和yield()的区别
sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。而yield方法调用后 ,是直接进入就绪状态,所以有可能刚进入就绪状态,又被调度到运行状态。
sleep方法声明抛出了InterruptedException,所以调用sleep方法的时候要捕获该异常,或者显示声明抛出该异常。而yield方法则没有声明抛出任务异常。
sleep方法比yield方法有更好的可移植性,通常不要依靠yield方法来控制并发线程的执行。
8.3 线程合并(join)
将几个并行线程的线程合并为一个单线程执行,应用场景是当一个线程必须等待另一个线程执行完毕才能执行时,join方法可以完成这个功能。
void join()
当前线程等该加入该线程后面,等待该线程终止。
void join(long millis)
当前线程等待该线程终止的时间最长为 millis 毫秒。 如果在millis时间内,该线程没有执行完,
那么当前线程进入就绪状态,重新等待cpu调度
void join(long millis,int nanos)
等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。如果在millis时间内,该线程没有执行完,
那么当前线程进入就绪状态,重新等待cpu调度
8.4 设置线程优先级
每个线程执行时都有一个优先级的属性,优先级高的线程可以获得较多的执行机会,而优先级低的线程则获得较少的执行机会。与线程休眠类似,线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的也并非没机会执行。
每个线程默认的优先级都与创建它的父线程具有相同的优先级,在默认情况下,main线程具有普通优先级。
注:Thread类提供了setPriority(int newPriority)和getPriority()方法来设置和返回一个指定线程的优先级,其中setPriority方法的参数是一个整数,范围是1~·0之间,也可以使用Thread类提供的三个静态常量:
- MAX_PRIORITY = 10
- MIN_PRIORITY = 1
- NORM_PRIORITY = 5
public class Test1 {
public static void main(String[] args) throws InterruptedException {
new MyThread("高级", 10).start();
new MyThread("低级", 1).start();
}
}
class MyThread extends Thread {
public MyThread(String name,int pro) {
super(name);//设置线程的名称
setPriority(pro);//设置线程的优先级
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(this.getName() + "线程第" + i + "次执行!");
}
}
}
8.5 守护线程(setDaemon)
守护线程是指在程序运行的时候在后台提供一种通用服务的线程
守护线程使用的情况较少,但并非无用,举例来说,JVM的垃圾回收、内存管理等线程都是守护线程。还有就是在做数据库应用时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监控连接个数、超时时间、状态等等。调用线程对象的方法setDaemon(true),则可以将其设置为守护线程。守护线程的用途为:
- 守护线程通常用于执行一些后台作业,例如在你的应用程序运行时播放背景音乐,在文字编辑器里做自动语法检查、自动保存等功能。
- Java的垃圾回收也是一个守护线程。守护线的好处就是你不需要关心它的结束问题。例如你在你的应用程序运行的时候希望播放背景音乐,如果将这个播放背景音乐的线程设定为非守护线程,那么在用户请求退出的时候,不仅要退出主线程,还要通知播放背景音乐的线程退出;如果设定为守护线程则不需要了。
setDaemon方法的详细说明:
public final void setDaemon(boolean on):将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。
该方法必须在启动线程前调用。 该方法首先调用该线程的 checkAccess 方法,且不带任何参数。这可能抛出 SecurityException(在当前线程中)。
参数:
on - 如果为 true,则将该线程标记为守护线程。
抛出:
IllegalThreadStateException - 如果该线程处于活动状态。
SecurityException - 如果当前线程无法修改该线程。
9. 线程同步
java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作前,被其他线程调用,从而保证该变量的唯一性和准确性。
9.1 同步方法
synchronized修饰方法,java每个对象都有一个内置锁,当用了synchronized修饰后,内置锁会保护整个方法。
public synchronized void save(){
}
synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类
9.2 同步代码块
synchronized关键字修饰的语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。
public class Bank {
private int count =0;//账户余额
//存钱
public void addMoney(int money){
synchronized (this) {
count +=money;
}
System.out.println(System.currentTimeMillis()+"存进:"+money);
}
//取钱
public void subMoney(int money){
synchronized (this) {
if(count-money < 0){
System.out.println("余额不足");
return;
}
count -=money;
}
System.out.println(+System.currentTimeMillis()+"取出:"+money);
}
//查询
public void lookMoney(){
System.out.println("账户余额:"+count);
}
}
9.3 使用特殊域变量(volatile)实现同步
- volatile关键字为域变量的访问提供了一种免锁机制;
- 使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新;
- 因此每次使用该域就要重新计算,而不是使用寄存器中的值;
- volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。
public class SynchronizedThread {
class Bank {
private volatile int account = 100;
public int getAccount() {
return account;
}
/**
* 用同步方法实现
*
* @param money
*/
public synchronized void save(int money) {
account += money;
}
/**
* 用同步代码块实现
*
* @param money
*/
public void save1(int money) {
synchronized (this) {
account += money;
}
}
}
class NewThread implements Runnable {
private Bank bank;
public NewThread(Bank bank) {
this.bank = bank;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
// bank.save1(10);
bank.save(10);
System.out.println(i + "账户余额为:" +bank.getAccount());
}
}
}
/**
* 建立线程,调用内部类
*/
public void useThread() {
Bank bank = new Bank();
NewThread new_thread = new NewThread(bank);
System.out.println("线程1");
Thread thread1 = new Thread(new_thread);
thread1.start();
System.out.println("线程2");
Thread thread2 = new Thread(new_thread);
thread2.start();
}
public static void main(String[] args) {
SynchronizedThread st = new SynchronizedThread();
st.useThread();
}
}
9.4 使用重入锁(Lock)实现线程同步
在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。
ReenreantLock类的常用方法有:
- ReentrantLock() : 创建一个ReentrantLock实例
- lock() : 获得锁
- unlock() : 释放锁
//只给出要修改的代码,其余代码与上同
class Bank {
private int account = 100;
//需要声明这个锁
private Lock lock = new ReentrantLock();
public int getAccount() {
return account;
}
//这里不再需要synchronized
public void save(int money) {
lock.lock();
try{
account += money;
}finally{
lock.unlock();
}
}
}