多线程
进程和线程
进程是程序的一次执行过程,是系统运行程序的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程
线程与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
多线程
多线程就是多个线程同时运行或交替运行。单核CPU的话是顺序执行,也就是交替运行。多核CPU的话,因为每个CPU有自己的运算器,所以在多个CPU中可以同时运行。
为什么需要多线程?
比如说,我们观看视频的网站,其中有画面显示,声音,还有弹幕,这些都是在同时执行的,我们看视频的时候,画面,声音,弹幕就需要多个线程来执行,他们同时执行,为我们提供了很好的体验。
线程实现
1.继承Thread类
(1)自定义线程类继承Thread类
(2)重写run()方法,编写线程执行体
(3)创建线程对象,调用start()方法启动线程
public class Test1 extends Thread{
@Override
public void run() {
for (int i = 0; i < 30; i++) {
System.out.println ("2222222");
}
}
public static void main(String[] args) {
Test1 test1 = new Test1 ();
test1.start ();
for (int i = 0; i < 1000; i++) {
System.out.println ("1111111");
}
}
}
2.实现Runnable接口
(1)实现接口Runnable具有多线程能力
(2)启动线程:传入目标对象+Thread对象.start()
public class Test3 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 30; i++) {
System.out.println ("2222222");
}
}
public static void main(String[] args) {
Test3 test3 = new Test3 ();
Thread thread = new Thread (test3);
thread.start ();
for (int i = 0; i < 1000; i++) {
System.out.println ("1111111");
}
}
}
当一个对象被多个线程使用的时候推荐使用这种方法
3.实现Callable接口
(1) 实现Callable接口,需要返回值类型
(2) 重写call方法,需要抛出异常
(3) 创建目标对象
(4) 创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1);
(5) 提交执行:Future result1 = ser.submit(t1);
(6) 获取结果:boolean r1 = result1.get();
(7) 关闭服务:ser.shutdownNow();
龟兔赛跑演示:
public class demo01 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建线程
ExecutorService ser = Executors.newFixedThreadPool(2);
Race tortoise = new Race("乌龟",1000);
Race rabbit = new Race("兔子",500);
Future<Integer> result1 = ser.submit(tortoise);
Future<Integer> result2 = ser.submit(rabbit);
Thread.sleep(2000);
tortoise.setFlag(false);
rabbit.setFlag(false);
// 获取值
Integer num1 = result1.get();
Integer num2 = result2.get();
System.out.println("乌龟"+num1);
System.out.println("兔子"+num2);
// 停止服务
ser.shutdownNow();
}
}
class Race implements Callable<Integer>{
private String name;//名称
private long time;//延时时间
private boolean flag = true;
private int step = 0;
public Race() {
}
public Race(String name) {
super();
this.name = name;
}
public Race(String name, long time) {
super();
this.name = name;
this.time = time;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public long getTime() {
return time;
}
public void setTime(long time) {
this.time = time;
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
public int getStep() {
return step;
}
public void setStep(int step) {
this.step = step;
}
@Override
public Integer call() throws Exception {
while (flag){
Thread.sleep(time);
step++;
}
return step;
}
}
4.使用线程池创建
自定义线程池,可以用ThreadPoolExecutor类创建,它有多个构造方法来创建线程池,然后创建任务,执行线程
public class ThreadPoolTest{
public static void main(String[] args){
//创建等待队列
BlockingQueue<Runnable> bqueue = new ArrayBlockingQueue<Runnable>(20);
//创建线程池,池中保存的线程数为3,允许的最大线程数为5
ThreadPoolExecutor pool = new ThreadPoolExecutor(3,5,50,TimeUnit.MILLISECONDS,bqueue);
//创建七个任务
Runnable t1 = new MyThread();
Runnable t2 = new MyThread();
Runnable t3 = new MyThread();
Runnable t4 = new MyThread();
Runnable t5 = new MyThread();
Runnable t6 = new MyThread();
Runnable t7 = new MyThread();
//每个任务会在一个线程上执行
pool.execute(t1);
pool.execute(t2);
pool.execute(t3);
pool.execute(t4);
pool.execute(t5);
pool.execute(t6);
pool.execute(t7);
//关闭线程池
pool.shutdown();
}
}
class MyThread implements Runnable{
@Override
public void run(){
System.out.println(Thread.currentThread().getName() + "正在执行。。。");
try{
Thread.sleep(100);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
一些常用的方法
currentThread()
返回对当前正在执行的线程对象的引用
getId()
返回此线程的标识符
getName()
返回此线程的名称
getPriority()
返回此线程的优先级
isAlive()
测试这个线程是否还处于活动状态
sleep(long millis)
使当前正在执行的线程以指定的毫秒数“休眠”(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性
setName(String name)
将此线程的名称更改为等于参数 name
join()
在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是 主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。
join()的作用是:“等待该线程终止”,这里需要理解的就是该线程是指的主线程等待子线程的终止。也就是在子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行
yield()
yield()方法的作用是放弃当前的CPU资源,将它让给其他的任务去占用CPU时间。注意:放弃的时间不确定,可能一会就会重新获得CPU时间片。
setPriority(int newPriority)
更改此线程的优先级
停止线程
不推荐使用JDK提供的 stop()、 destroy()方法。【已废弃】
推荐线程自己停止下来
建议使用一个标志位进行终止变量
当flag=false,则终止线程运行。
线程睡眠
sleep (时间) 指定当前线程阻塞的毫秒数;
sleep存在异常InterruptedException;
sleep时间达到后线程进入就绪状态;
sleep可以模拟网络延时,倒计时等。
每一个对象都有一个锁,sleep不会释放锁;
上面龟兔赛跑的例子就使用了sleep方法,使兔子走的这条线程睡眠,模仿龟兔赛跑中,兔子在半途睡觉的场景
Join
Join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞
当调用join的时候,就会执行调用的线程,直到此线程结束,再去执行其他线程
线程优先级
Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度 器按照优先级决定应该调度哪个线程来执行
线程的优先级用数字表示,范围从1~10.
Thread.MIN_PRIORITY = 1;
Thread.MAX_PRIORITY = 10;
Thread.NORM_PRIORITY = 5
注意事项:
优先级低只是意味着获得调度的 概率低.并不是优先级低就不会 被调用了.这都是看CPU的调度
多线程分类
用户线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程
守护线程:运行在后台,为其他前台线程服务.也可以说守护线程是JVM中非守护线程的 “佣人”。
特点:一旦所有用户线程都结束运行,守护线程会随JVM一起结束工作
应用:数据库连接池中的检测线程,JVM虚拟机启动后的检测线程
最常见的守护线程:垃圾回收线程
public class Run {
public static void main(String[] args) {
try {
MyThread thread = new MyThread();
thread.setDaemon(true);
thread.start();
Thread.sleep(5000);
System.out.println("我离开thread对象也不再打印了,也就是停止了!");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
class MyThread extends Thread {
private int i = 0;
@Override
public void run() {
try {
while (true) {
i++;
System.out.println("i=" + (i));
Thread.sleep(100);
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
线程同步
并发 : 同一个对象被多个线程同时操作
处理多线程问题时 , 多个线程访问同一个对象 , 并且某些线程还想修改这个对象 . 这时候我们就需要线程同步 . 线程同步其实就是一种等待机制 , 多个需要同时访问 此对象的线程进入这个对象的等待池 形成队列, 等待前面线程使用完毕 , 下一个线 程再使用
实现方法
队列+锁
由于同一进程的多个线程共享同一块存储空间 , 在带来方便的同时,也带来了访问 冲突问题 , 为了保证数据在方法中被访问时的正确性 , 在访问时加入 锁机制 synchronized , 当一个线程获得对象的排它锁 , 独占资源 , 其他线程必须等待 , 使用后释放锁即可 . 存在以下问题:
一个线程持有锁会导致其他所有需要此锁的线程挂起 ;
在多线程竞争下 , 加锁 , 释放锁会导致比较多的上下文切换 和 调度延时,引 起性能问题 ;
如果一个优先级高的线程等待一个优先级低的线程释放锁 会导致优先级倒 置 , 引起性能问题 .
同步方法:
由于我们可以通过 private 关键字来保证数据对象只能被方法访问 , 所以我们只需 要针对方法提出一套机制 , 这套机制就是 synchronized 关键字 , 它包括两种用法 : synchronized 方法 和synchronized 块 .
同步方法: public synchronized void method(int args) {}
synchronized方法控制对 “对象” 的访问 , 每个对象对应一把锁 , 每个 synchronized方法都必须获得调用该方法的对象的锁才能执行 , 否则线程会阻塞 , 方法一旦执行 , 就独占该锁 , 直到该方法返回才释放锁 , 后面被阻塞的线程才能获 得这个锁 , 继续执行
synchronized方法的缺点
使用synchronized关键字声明方法有些时候是有很大的弊端的,比如我们有两个线程一个线程A调用同步方法后获得锁,那么另一个线程B就需要等待A执行完,但是如果说A执行的是一个很费时间的任务的话这样就会很耗时
public class Run {
public static void main(String[] args) {
Task task = new Task();
MyThread1 thread1 = new MyThread1(task);
thread1.start();
MyThread2 thread2 = new MyThread2(task);
thread2.start();
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
long beginTime = CommonUtils.beginTime1;
if (CommonUtils.beginTime2 < CommonUtils.beginTime1) {
beginTime = CommonUtils.beginTime2;
}
long endTime = CommonUtils.endTime1;
if (CommonUtils.endTime2 > CommonUtils.endTime1) {
endTime = CommonUtils.endTime2;
}
System.out.println("耗时:" + ((endTime - beginTime) / 1000));
}
}
class MyThread1 extends Thread {
private Task task;
public MyThread1(Task task) {
super();
this.task = task;
}
@Override
public void run() {
super.run();
CommonUtils.beginTime1 = System.currentTimeMillis();
task.doLongTimeTask();
CommonUtils.endTime1 = System.currentTimeMillis();
}
}
class MyThread2 extends Thread {
private Task task;
public MyThread2(Task task) {
super();
this.task = task;
}
@Override
public void run() {
super.run();
CommonUtils.beginTime2 = System.currentTimeMillis();
task.doLongTimeTask();
CommonUtils.endTime2 = System.currentTimeMillis();
}
}
class CommonUtils {
public static long beginTime1;
public static long endTime1;
public static long beginTime2;
public static long endTime2;
}
可以使用synchronized同步块来解决这个问题。但是要注意synchronized同步块的使用方式,如果synchronized同步块使用不好的话并不会带来效率的提升。
public class Task {
private String getData1;
private String getData2;
public void doLongTimeTask() {
try {
System.out.println("begin task");
Thread.sleep(3000);
String privateGetData1 = "长时间处理任务后从远程返回的值1 threadName="
+ Thread.currentThread().getName();
String privateGetData2 = "长时间处理任务后从远程返回的值2 threadName="
+ Thread.currentThread().getName();
synchronized (this) {
getData1 = privateGetData1;
getData2 = privateGetData2;
}
System.out.println(getData1);
System.out.println(getData2);
System.out.println("end task");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
从上面代码可以看出当一个线程访问一个对象的synchronized同步代码块时,另一个线程任然可以访问该对象非synchronized同步代码块。
死锁
多个线程各自占有一些共享资源 , 并且互相等待其他线程占有的资源才能运行 , 而 导致两个或者多个线程都在等待对方释放资源 , 都停止执行的情形 . 某一个同步块 同时拥有 “ 两个以上对象的锁 ” 时 , 就可能会发生 “ 死锁 ” 的问题
产生死锁的必要条件
- 互斥条件:一个资源每次只能被一个进程使用。
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件 : 进程已获得的资源,在末使用完之前,不能强行剥夺。
- 循环等待条件 : 若干进程之间形成一种头尾相接的循环等待资源关系
死锁演示
public class ThreadDeadlock {
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(5000);
t2.start();
Thread.sleep(5000);
t3.start();
}
}
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();
System.out.println(name + " acquiring lock on "+obj1);
synchronized (obj1) {
System.out.println(name + " acquired lock on "+obj1);
work();
System.out.println(name + " acquiring lock on "+obj2);
synchronized (obj2) {
System.out.println(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(30000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
怎么避免死锁
1、避免嵌套锁
这是死锁最常见的原因,如果您已经持有一个资源,请避免锁定另一个资源。如果只使用一个对象锁,则几乎不可能出现死锁情况,比如以下代码对上边的循环嵌套部分进行修改,则避免了死锁的情况:
public void run() {
String name = Thread.currentThread().getName();
System.out.println(name + " acquiring lock on " + obj1);
synchronized (obj1) {
System.out.println(name + " acquired lock on " + obj1);
work();
}
System.out.println(name + " released lock on " + obj1);
System.out.println(name + " acquiring lock on " + obj2);
synchronized (obj2) {
System.out.println(name + " acquired lock on " + obj2);
work();
}
System.out.println(name + " released lock on " + obj2);
System.out.println(name + " finished execution.");
}
2、只锁需要的部分
只获对需要的资源加锁,例如在上面的程序中,我们锁定了完整的对象资源,但是如果我们只需要其中一个字段,那么我们应该只锁定那个特定的字段而不是完整的对象
Lock锁
从JDK 5.0开始,Java提供了更强大的线程同步机制——通过显式定义同步锁对 象来实现同步。同步锁使用Lock对象充当
java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。 锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开 始访问共享资源之前应先获得Lock对象
ReentrantLock 类实现了 Lock ,它拥有与 synchronized 相同的并发性和内存语 义,在实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、释 放锁。
class A{
private final ReentrantLock lock = new ReenTrantLock();
public void m(){ lock.lock();
try{ //保证线程安全的代码;
}
finally{
lock.unlock(); //如果同步代码有异常,要将unlock()写入finally语句块
}
}
}
synchronized 与 Lock 的对比
Lock是显式锁(手动开启和关闭锁,别忘记关闭锁)synchronized是隐式锁,出了 作用域自动释放
Lock只有代码块锁,synchronized有代码块锁和方法锁
使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展 性(提供更多的子类)
优先使用顺序 :
Lock > 同步代码块(已经进入了方法体,分配了相应资源)> 同步方法(在方 法体之外)
线程通信
应用场景 : 生产者和消费者问题
假设仓库中只能存放一件产品 , 生产者将生产出来的产品放入仓库 , 消费者将 仓库中产品取走消费 .
如果仓库中没有产品 , 则生产者将产品放入仓库 , 否则停止生产并等待 , 直到 仓库中的产品被消费者取走为止 .
如果仓库中放有产品 , 则消费者可以将产品取走消费 , 否则停止消费并等待 , 直到仓库中再次放入产品为止 .
分析:
这是一个线程同步问题 , 生产者和消费者共享同一个资源 , 并且生产者和消费者之 间相互依赖 , 互为条件 .
对于生产者 , 没有生产产品之前 , 要通知消费者等待 . 而生产了产品之后 , 又 需要马上通知消费者消费
对于消费者 , 在消费之后 , 要通知生产者已经结束消费 , 需要生产新的产品 以供消费.
在生产者消费者问题中 , 仅有synchronized是不够的
synchronized 可阻止并发更新同一个共享资源 , 实现了同步 u synchronized 不能用来实现不同线程之间的消息传递 (通信)
Java提供了几个方法解决线程之间的通信问题
方法名称 | 描述 |
---|---|
notify() | 随机唤醒等待队列中等待同一共享资源的 “一个线程”,并使该线程退出等待队列,进入可运行状态,也就是notify()方法仅通知“一个线程 |
notifyAll() | 使所有正在等待队列中等待同一共享资源的 “全部线程” 退出等待队列,进入可运行状态。此时,优先级最高的那个线程最先执行,但也有可能是随机执行,这取决于JVM虚拟机的实现 |
wait() | 使调用该方法的线程释放共享资源锁,然后从运行状态退出,进入等待队列,直到被再次唤醒 |
wait(long) | 超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回 |
wait(long,int) | 对于超时时间更细力度的控制,可以达到纳秒 |
解决方式1
并发协作模型 “ 生产者 / 消费者模式 ” —>管程法
生产者 : 负责生产数据的模块 (可能是方法 , 对象 , 线程 , 进程) ;
消费者 : 负责处理数据的模块 (可能是方法 , 对象 , 线程 , 进程) ;
缓冲区 : 消费者不能直接使用生产者的数据 , 他们之间有个 “ 缓冲区 生产者将生产好的数据放入缓冲区 , 消费者从缓冲区拿出数据
class Resource {
//当前资源的数量
int num = 0;
//当前资源的上限
int size = 10;
//消费资源
public synchronized void remove() {
//如果num为0,没有资源了,需要等待
while (num == 0) {//这里jdk源码里推荐用while,因为有可能出现虚假唤醒,所以要再次确认
try {
System.out.println("消费者进入等待");
this.wait();//线程等待,并释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果线程可以执行到这里,说明资源里有资源可以消费
num--;
System.out.println("消费者线程为:" + Thread.currentThread().getName() + "--资源数量:" + num);
this.notify();//唤醒其他正在等待的线程
}
//生产资源
public synchronized void put() {
//如果资源满了,就进入阻塞状态
while (num == size) {//这里jdk源码里推荐用while,因为有可能出现虚假唤醒,所以要再次确认
try {
System.out.println("生产者进入等待");
this.wait();//线程进入阻塞状态,并释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
num++;
System.out.println("生产者线程为:" + Thread.currentThread().getName() + "--资源数量:" + num);
this.notify();//唤醒其他正在等待的线程
}
}
//消费者
class Consumer implements Runnable {
private Resource resource;
public Consumer(Resource resource) {
this.resource = resource;
}
@Override
public void run() {
while (true){
resource.remove();
}
}
}
//生产者
class Producer implements Runnable {
private Resource resource;
public Producer(Resource resource){
this.resource=resource;
}
@Override
public void run() {
while (true){
resource.put();
}
}
}
public class TestConsumerAndProducer {
public static void main(String[] args) {
Resource resource = new Resource();
//生产线程
Producer p1 = new Producer(resource);
//消费线程
Consumer c1 = new Consumer(resource);
new Thread(p1).start();
new Thread(c1).start();
}
}
解决方式 2
并发协作模型 “ 生产者 / 消费者模式 ” —>信号灯法