一、多线程
1. 多线程实现的两种方式
1).extends Thread
//1. 继承Thread类
public class MyThread extends Thread{
//2. 重写run()方法
@Override
public void run(){
//code
}
}
public static void main(String[] args){
//3. 创建MyThread对象
MyThread myThread = new MyThread();
//4. 调用start()方法
myThread.start();
}
2).implements Runnable
//1. 实现Runnable接口
public class MyRunnable implements Runnable{
//2. 重写run()方法
@Override
public void run(){
//code
}
}
public static void main(String[] args){
//3. 创建MyRunnable对象
MyRunnable myRunnable = new MyRunnable();
//4. 创建Thread对象
/*
Thread类中存在两个构造方法:
a. Thread(Runnable runnable);
b. Thread(Runnbale runnable,String threadName);
*/
Thread thread = new Thread(myRunnable);
//5. 调用start()方法
thread.start();
}
2. Thread和Runnable的区别
a. implements Runnable避免了单继承的局限性
b. 增强了程序的扩展性,降低了程序的耦合性(解耦):
将设置线程任务和开启新线程进行了分离(解耦)
二、线程安全
1.线程安全问题描述
当多线程程序访问共享数据时,会出现线程安全问题
2.线程安全问题产生图解

3.线程的六种状态
Thread.State

三、线程安全解决办法
1.同步代码块
1).代码格式
synchronized(同步锁){
需要同步操作的代码,即可能出现线程安全问题的代码
}
2).注意事项
a. 同步代码块中的锁对象,可以使用任意的对象
b. 必须保证多个线程使用的锁对象是同一个
c.锁对象的作用:
把同步代码块锁住,只让一个线程在同步代码块中执行
3).同步代码块的技术原理
使用了一个锁对象,也叫对象锁,也叫对象监视器
第一个线程抢到了cpu执行权,执行run()方法,遇到synchronized(锁对象){}代码块,检查其中是否有锁对象,如果有,就进入同步代码块执行
此时,如果第二个线程抢到了cpu执行权,执行run()方法,遇到synchronized(锁对象){}代码块,检查其中是否有锁对象,发现没有(第一个线程没执行完成,还未归还锁对象),则进入阻塞状态,直到获取到第一个线程执行完成归还的锁对象,才能进入到同步代码块中执行
总结:
a. 同步代码块中的线程,没有执行完毕不会释放锁
b. 同步代码块外的线程,没有锁不能进入执行
4). 同步代码块存在的问题
程序频繁地判断锁、获取锁、释放锁降低效率
2.同步方法
1). 代码格式
public synchronized void payTicket(){
//可能出现线程安全问题的代码(访问了共享数据的代码)
}
2). 同步方法技术原理
定义一个同步方法,也会把方法内部的代码锁住,只让一个线程执行,同步方法的锁对象就是实现类对象,就是this
3). 与同步代码块的辨析
a. 同步代码块中显式使用锁对象进行线程同步
b. 同步方法中隐式使用实现接口的类对象,即this作为锁对象
4). 静态同步方法
a. 格式
public class MyRunnable implements Runnable{
//静态方法只能访问静态变量
static int ticket = 100;
@Override
public void run(){
while(true){
if(ticket > 0){
sellTicketStatic();
}else{
break;
}
}
}
public static synchronized void sellTicketStatic(){
if(ticket > 0){
System.out.println("正在售卖第"+(ticket--)+"张票");
}
}
}
b. 注意
a).
new Thread(Runnable实现类)对象即使调用了start()方法,也并不是一口气把run()方法跑完,因此执行一段时间sellTicketStatic()方法后,会出现新的线程抢占执行权的情况,继续再执行run()方法调用的sellTicketStatic()方法,因此sellTicketStatic()中不能写while()循环,否则就变成一个线程抢占到执行权,一直执行完毕(单线程)
b).静态同步方法的锁对象不是this,因为静态方法早于对象创建而加载的,因此,其锁对象是本类的class属性,即class文件对象(反射)
synchronized(MyRunnable.class){ //同步代码块 }
3.锁机制
JDK1.5之后,新增java.util.concurrent.locks.Lock接口
1).意义
Lock实现了比使用同步的方法更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可支持多个相关的Condition对象
2).常用方法
a. void lock()获取锁 b. void unlock()释放锁
3).Lock接口实现类
java.util.concurrent.locks.Reentrantlock implements Lock
4).使用步骤
a. 在类中成员位置处创建Reentrantlock类对象
b. 在可能出现线程安全问题代码前调用Lock接口的lock()方法获取锁
c. 在可能出现线程安全问题代码后调用Lock接口的unlock()方法释放锁
5).代码解析
a. 错误代码
public class MyRunnable implements Runnable{
int ticket =100;
Lock lock = new ReentrantLock();
@Override
public void run(){
while(true){
lock.lock();
if(ticket > 0){
try{
Thread.sleep(100);
System.out.println("正在售卖第"+(ticket--)+"张票");
}catch(Exception e){
e.printStackTrace();
}finally{
//无论是否出现异常,都会将锁释放掉
lock.unlock();
}
}else{
System.out.println("当前进程未结束"+ Thread.currentThread().getName());
break;
}
}
}
}
b. 代码更正
public class MyRunnable implements Runnable{
int ticket =100;
Lock lock = new ReentrantLock();
@Override
public void run(){
while(true){
lock.lock();
if(ticket > 0){
try{
Thread.sleep(100);
System.out.println("正在售卖第"+(ticket--)+"张票");
}catch(Exception e){
e.printStackTrace();
}finally{
//无论是否出现异常,都会将锁释放掉
lock.unlock();
}
}else{
lock.unlock();
System.out.println("当前进程已结束"+ Thread.currentThread().getName());
break;
}
}
}
}
c. 原因解析
a中的代码错误在于:
当一个新的线程抢占到执行权时,如果刚好此时
ticket <= 0,进入else语句,线程未解锁,执行了打印语句,但无法执行break语句,具体原因未知
b中更正代码解释:
线程执行
run()方法时,一个新的线程抢占到执行权后,当前线程会调用finally中的代码释放锁,以让新的线程继续执行,考虑另一种情况,如果此时刚好ticket <= 0,则新线程释放锁并跳出循环,问题在于创建的三个线程都会执行else{}中的代码,因为他们共同写入了MyRunnable参数,主函数中没有代码告诉他们已经可以跳出循环了,三个线程分别跑到run()方法中,发现可以跳出循环,故需要线程间通信
四、等待唤醒机制(线程间通信)
1. 定义
多个线程处理同一个资源,但各线程的线程任务不同,需要有规律执行各线程
Tips: 可理解为合作关系,比如一个线程生产包子,另一个线程吃包子,如果多个线程处理同一个资源,且任务相同,类似于竞争关系,上述买票即如此,需要三个窗口线程分别确认是否跳出循环
2. 概述
任务不同的各线程通过判断共享资源的状态执行等待或执行操作
3. 常用方法
1). wait()
线程不再抢夺CPU执行权,进入线程所属对象的wait set中,不浪费CPU资源,不竞争锁,线程状态是WAITING。当前线程需要等待别的线程执行通知notify()方法在当前线程所属对象上,使其从wait set中释放出来,重新进入到调度队列(ready queue)中
2). notify()
选取所通知对象的wait set中的一个线程释放,一般选取其中等待时间最长的线程
3). notifyAll()
释放所通知对象
的wait set中的全部线程
4). wait(long)
线程进入TimeWaiting状态,参数long时间后,自动唤醒
5). sleep(long)
线程进入TimeWaiting状态,参数long时间后,自动开启
4. 注意
通知一个等待的线程,该线程也不会立即恢复执行,因为中断的位置在同步块内,而此时他并未持有锁,需要再次尝试获取锁(依旧面临其他线程的竞争),竞争到锁后才能在当初调用wait()方法之后的地方恢复执行
Tips:
a. 如果线程获取到锁,则从WAITING状态变成RUNNABLE状态
b. 否则,从wait set出来,又进入entry set,线程从WAITING(等待)状态变成BLOCKED(阻塞)状态
5. 方法调用细节
1). wait()方法与notify()方法必须由同一个锁对象调用。因为对应的锁对象可以通过notify()唤醒使用该锁对象调用wait()方法以等待的线程
2). wait()方法与notify()方法同属于Object类
3). wait()方法与notify()方法必须要在同步代码块或者同步方法中使用,因为需要使用锁对象调用这两个方法
五、线程池
1. 意义
如果线程数量很多,而任务简单,频繁创建线程实则降低效率
需要线程复用,执行完线程并不被销毁,而是转而执行其他任务
2. 概念
容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,避免消耗过多资源
3. 本质
LinkedList
JDK1.5之后,内置了线程池函数
4. 详述
-
java.util.concurrent.Executors:线程池工厂类,用来生成线程池 -
Executors类中静态方法:static ExecutorService newFixedThreadPool(int nTheads):创建一个可复用固定线程数的线程池 -
params:
nThreads创建线程池中包含的线程数量 -
returns:
ExecutorService接口,返回的是ExecutorService接口的实现类对象,我们可以使用ExecutorService接口接收(面向接口编程) -
java.util.concurrent.ExecutorService:线程池接口,其中有一个方法用来从线程池中获取线程,调用start方法,执行线程任务:submit(Runnable task)提交一个Runnable任务用于执行 -
关闭/销毁线程池的方法
void shutdown()
5. 使用
- 使用线程池的工厂类
Executors里提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池 - 创建一个类,实现Runnbale接口,重写
run()方法,设置线程任务 - 调用
ExecutorService中的submit( Runnable task)方法,传递线程任务,开启线程,执行run()方法 - 可以但不建议调用
executorService中的shutdown()方法关闭/销毁线程池