Java——多线程
并发与并行
并发:指两个或多个事件在同一个时间段内发生 交替进行
并行:指两个或多个时间在同一时刻发生 同时发生
进程与线程
进程:一个内存中运行的应用程序 进入内存执行的程序
线程:属于进程,是进程中的一个执行单位,负责当前进程中程序的执行
线程的调度
分时调度
抢占式调度(Java)
创建多线程程序
Thread类
java.lang.Thread
实现步骤
-
创建一个Thread类的子类
-
在Thread类的子类中重写Thread类中的run方法,设置线程任务
-
创建Thread类的子类对象
-
调用Thread类中的方法start,开启新线程,自动执行run方法
void start() 导致此线程开始执行; Java虚拟机调用此线程的run方法
结果是两个线程同时运行:当前线程(主线程 → 从调用返回到start方法)和另一个线程(新线程 → 执行其run方法)。
不止一次启动线程是不合法的。特别地,一旦线程完成执行就可能不会重新启动。
常用方法
获取线程的名称
-
使用Thread类中的方法getName()
String getName() 返回此线程的名称
-
可以先获取到当前正在执行的线程,使用线程中的方法currentThread()
static Thread currentThread() 返回对当前正在执行的线程对象的引用
设置线程的名称
-
使用Thread类中的setName(名字)
void setName(String name) 将此线程的名称更改为等于参数 name
-
创建一个带参数的构造方法,参数传递线程的名称;调用父类的带参构造方法,把线程名称传递给父类,让父类(Thread)给子线程取一个名字
Thread(String name) 分配一个新的 Thread对象
sleep()
static void sleep(long millis) 使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性
注意是静态方法
Runnable接口
java.lang.runnable
Runnable接口应由任何类实现,其实例将由线程执行。 该类必须定义一个无参数的方法,称为run 。
java.lang.Thread类的构造方法:
Thread(Runnable target) 分配一个新的 Thread对象
Thread(Runnable target, String name) 分配一个新的 Thread对象
实现步骤
-
创建一个Runnable接口的实现类
-
在实现类中重写Runnable接口的run方法,设置线程任务
-
创建一个Runnable接口实现类的对象
-
创建Thread类对象,构造方法中传递Runnable接口的实现类对象
-
调用Thread类中的start方法,开启新的线程执行run方法
Thread类和Runnable接口的区别
实现Runnable接口创建多线程程序的好处:
-
避免了单继承的局限性
一个类只能继承一个类 实现了Runnable接口还可以实现其他的接口
-
增强程序的拓展性,降低了程序的耦合性(解耦)
实现Runnable接口的方式,把设置线程任务和开启线程任务进行了分类(解耦)
实现类中,重写了run方法:用来设置线程任务
常见Thread类对象,调用start方法:用来开启线程
多线程原理
匿名内部类
作用:简化代码
把实现类实现接口,重写接口的方法,创建实现类对象合成一步完成
匿名内部类的最终产物:子类/实现类对象,而这个类没有名字
格式
new 父类/接口(){
重写父类/接口的方法
};
1 new Thread(){ 2 public void run(){ 3 for(int i = 0 ; i < 20; i++){ 4 System.out.println(Thread.currentThread().getName()); 5 } 6 } 7 }.start();
1 Runnable r = new Runnable(){ 2 public void run(){ 3 for(int i = 0 ; i < 20; i++){ 4 System.out.println(Thread.currentThread().getName()); 5 } 6 } 7 } 8 9 new Thread(r).start(); 10 11 //简化版 12 new Thread(new Runnable(){ 13 public void run(){ 14 for(int i = 0 ; i < 20; i++){ 15 System.out.println(Thread.currentThread().getName()); 16 } 17 } 18 }).start();
线程同步
问提分析
1 public class RunnableImpl implements Runnable{ 2 3 private int ticket = 100; 4 5 public void run() { 6 while(ticket > 0) { 7 // try { 8 // Thread.sleep(10); 9 // } catch (InterruptedException e) { 10 // e.printStackTrace(); 11 // } 12 System.out.println(Thread.currentThread().getName()+"在卖第"+ticket+"票"); 13 ticket--; 14 } 15 } 16 } 17 18 public class Mian { 19 20 public static void main(String[] args) { 21 22 RunnableImpl run = new RunnableImpl(); 23 24 Thread t0 = new Thread(run); 25 Thread t1 = new Thread(run); 26 Thread t2 = new Thread(run); 27 28 t0.start(); 29 t1.start(); 30 t2.start(); 31 } 32 33 }
运行结果:
1 Thread-2在卖第100票 2 Thread-1在卖第100票 3 Thread-0在卖第98票 4 Thread-1在卖第97票 5 Thread-2在卖第97票 6 Thread-0在卖第95票 7 Thread-2在卖第94票 8 Thread-1在卖第94票 9 ...... 10 Thread-0在卖第2票 11 Thread-2在卖第1票 12 Thread-1在卖第1票 13 Thread-0在卖第-1票
发现不同的线程会卖相同的票号以及不存在的票号
解决方法:
使用线程同步
同步代码块
格式
synchronized(锁对象){
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
注意
-
通过代码块中的锁对象,可以使用任意的对象
-
但是必须保证多个线程使用的锁对象是同一个
-
锁对象作用:
把同步代码块锁住,只让一个线程在同步代码块中执行
原理
使用了一个锁对象,这个锁也叫同步锁/锁对象/对象监视器
同步中的线程没有执行完毕不会释放锁对象,同步外的的线程没有获取到锁对象进不去同步
优缺点
优点:同步保证了只能有一个线程在同步中执行共享数据,保证了安全
缺点:程序频繁判断锁、释放锁、获取锁,程序的效率会降低
同步方法
格式
定义方法的格式
修饰符 synchronized 返回值类型 方法名 (参数列表) {
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
原理
定义一个同步方法,同步方法也会把方法内的代码锁住,只让一个线程执行
锁对象是实现类对象(实现Runnable接口的对象)
锁机制
java.util.locks.Lock接口
Lock实现提供比使用synchronized方法和语句可以获得的更广泛的锁定操作。
Lock接口中的方法
void lock() 获得锁
void unlock() 释放锁
java.util.concurrent.locks.ReentrantLock类 implements Lock接口
使用步骤
-
在成员位置处创建一个ReentrantLock对象
-
在可能出现安全的代码前调用Lock接口中的方法lock获取锁
-
在可能出现安全的代码后调用Lock接口中的方法unlock释放锁
线程的状态
状态名称 | 状态描述 |
---|---|
NEW | 尚未启动的线程处于此状态 |
RUNNABLE | 在Java虚拟机中执行的线程处于此状态 |
BLOCKED | 被阻塞等待监视器锁定的线程处于此状态 |
WAITING | 正在等待另一个线程执行特定动作的线程处于此状态 |
TIMED_WAITING | 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态 |
TERMINATED | 已退出的线程处于此状态 |
TIMED_WAITING状态
进入此状态的两种方式:
-
使用sleep(long m)方法,线程睡醒后进入到Runnable/Blocked状态
-
使用wait(long m)方法,wait方法如果在毫秒值结束之后,还没有被notify唤醒,就会自动醒来,线程进入到Runnable/Blocked状态
唤醒的方法:
-
void notify() 唤醒正在等待对象监视器的单个线程(如果有多个等待的线程就随机唤醒一个等待的线程)
-
void notifyAll() 唤醒正在等待对象监视器的所有线程
线程间的通信
为什么要处理线程间的通信?
多个线程并发执行时,在默认的情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一项任务时,并且我们希望他们有规律地执行,那么多线程之间需要一些协调通信,以此来达到多线程处理一份数据地要求。
如何保证线程之间通信、有效利用资源?
等待唤醒机制
等待唤醒机制
即线程之间的通信
机制中的方法
-
wait()
-
notify()
-
notifyAll()
调用wait()和notify()方法的注意事项
-
wait方法和notify方法必须要由同一个锁对象调用
对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程
-
wait方法和notify方法是属于Object类的方法
锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的
-
wait方法和notify方法必须要在同步代码或是同步方法中使用
必须通过锁对象调用这两个方法
线程池
概念:一个容纳多个线程的容器,其中的线程可以反复使用,省去了繁杂创建线程对象的操作,无需反复创建线程而消耗过多资源
Executors类
java.util.concurrent.Executors 线程池的工厂类,用来生成线程池
静态方法
static ExecutorService newFixedThreadPool(int nThreads) 创建一个线程池,该线程池重用固定数量的从共享无界队列中运行的线程
参数
int nThreads:创建线程池中包含的线程数量
返回值
ExecutorService接口,返回的是ExecutorService 接口中的实现类对象,我们可以使用ExecutorService 接口来接收(面向接口编程)
ExecutorService接口
java.util.concurrent.ExecutorService 线程池接口
用来从线程池获取线程,调用start方法,执行线程任务
方法
Future<?> submit(Runnable task) 提交一个可运行的任务执行,并返回一个表示该任务的未来
void shutdown() 启动有序关闭,其中先前提交的任务将被执行,但不会接受任何新任务
使用步骤
-
使用线程池的工厂类Executors里提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
-
创建一个类,实现Runnable接口,重写run方法,设置线程任务
-
调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
-
调用ExecutorService中的方法shutdown销毁线程(不建议使用)
Lambda表达式
函数式编程思想
面向对象的思想:
做一件事情要找一个能解决这个事情的对象调用对象的方法,完成事情
函数式编程思想:
只要能获取到结果,谁去做的或者是怎么做的都不重要,重视的是结果,不重视过程
Lambda更优写法
1 //使用匿名内部类的方式 实现多线程 2 new Thread(new Runnable() { 3 public void run() { 4 System.out.println(Thread.currentThread().getName()+"新线程建立了"); 5 } 6 }).start(); 7 8 //使用Lambda表达式 实现多线程 9 new Thread(()-> { 10 System.out.println(Thread.currentThread().getName()+"新线程建立了"); 11 }).start();
1 () -> {System.out.println(Thread.currentThread().getName()+"新线程建立了");}
前面的一对小括号()即run方法的参数(这里没有),代表不需要任何条件
中间的一个箭头 ->代表将前面的参数传递给后面的代码
后面的输出语句即业务逻辑代码
Lambda标准格式
-
一些参数
-
一个箭头
-
一段代码
(参数列表) -> {重写方法的代码}
解释说明格式
() : 接口中抽象方法的参数列表,没有参数就空着;有参数就写出参数,多个参数之间用逗号分隔
-> : 传递的意思,把参数传给方法体
{} : 重写接口的抽象方法
Lambda表达式省略格式
凡是根据上下文推导出来的内容,都可以省略书写
可以省略的内容:
-
(参数列表) : 括号中参数列表的数据类型可以省略不写
-
(参数列表) : 括号中的参数如果只有一个,那么类型和括号都可以省略
-
{一些代码} : 如果{}中的代码只有一行,无论是否有返回值,都可以省略{} return ;