java学习---多线程
Posted 易小顺
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java学习---多线程相关的知识,希望对你有一定的参考价值。
文章目录
多线程
1、 简介
学习多线程之前,我们先要了解一些相关的名词含义,线程、进程、多线程…。
1.1 名词解释
-
进程
进程
Process
是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是 操作系统 结构的基础,也可以认为是一个正在运行的 程序实例 。 一个进程中 至少 有一个线程存活。 -
线程
线程
thread
是 操作系统 能够进行运算调度的 最小单位 。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,是一条单独的执行路径,一个进程中可以并发多个线程,每条线程并行执行不同的任务。 -
多线程
多线程
multithreading
,是指从软件或者硬件上实现多个线程并发执行的技术,也就是一个进行中多个线程并发执行,每个线程之间可以相互切换。 -
异步、同步
异步:多个线程线程同时执行 , 效率高但是数据不安全。
同步:多个线程间排队执行 , 效率低但是安全。
-
并发、并行
并发:指两个或多个事件在同一个时间段内发生。
并行:指两个或多个事件在同一时刻发生(同时发生)。
-
守护线程、用户线程
守护线程可以看成是用户线程的 保姆,只要有一个用户线程还在运行,所有的守护线程都会运行;只要当前进程之中没有用户线程运行了,所有 守护线程自动死亡退出 ,进程结束,退出程序。
-
公平锁、非公平锁
先来后到的进行取锁为公平锁,所有线程一起抢锁为非公平锁。可通过创建锁时传入
fair = true
表示为公平锁,默认是非公平锁。
1.2 线程调度
-
分时调度
所有线程轮流使用
CPU
的使用权,平均分配每个线程占用CPU
的时间。 -
抢占式调度
优先让优先级高的线程使用 CPU
,如果线程的优先级相同,那么会随机选择一个(线程随机性), Java
使用的为 抢占式调度。
-
多线程执行机制
CPU
使用抢占式调度模式在多个线程间进行着高速的切换。对于CPU
的一个核而言,某个时刻, 只能执行一个线程,而CPU
的在多个线程间切换速度相对我们的感觉要快,看上去就是 在同一时刻运行。 其实,多线程程序并不能提高程序的运行速度,但能够提高程序运行效率,让CPU
的使用率更高。
1.3 线程六个状态
-
new
线程已经创建成功但是还未启动。 从新建一个线程对象到程序
start()
这个线程之间的状态,都是新建状态; -
runnable
线程正在执行中。 就绪状态下的线程在获取
CPU
资源后就可以执行run()
,此时的线程便处于运行状态。 -
blocked
线程排队等待资源中。 线程对象调用
start()
方法后,就处于就绪状态,等到JVM里的线程调度器的调度; -
waiting
线程休眠。在一个线程执行了
sleep
(睡眠)、suspend
(挂起)等方法后会失去所占有的资源,从而进入阻塞状态,在睡眠结束后可重新进入就绪状态。 -
TIMED_WAITING
等待时间休眠中,时间结束后会自动进入等待状态。
-
TERMINATED
线程退出执行的状态。
run()
方法完成后或发生其他终止条件时就会切换到终止状态。
1.4 多线程的意义
- 优点
优点 | |
---|---|
更好的利用cpu 资源 | 如果只有一个线程执行,则第二个任务必须等到第一个任务结束后才能进行。如果使用多线程则在主线程执行任务的同时可以切换执行其他任务,而不需要等待其他任务完成。 |
数据共享 | 各个进程之间的数据是独立的,但是线程之间除了独立的资源区外,还有资源可以共享。 |
开发简单 | Java 内置多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了Java 的多线程编程的开发。 |
-
缺点
一是
上下文之间的切换时会带来 多余 的开销,在某些情况下反而会降低程序的性能,没有单线程执行的效率高。时间片是CPU分配给各个线程的时间,因为时间非常短,所以CPU不断通过切换线程,让我们觉得多个线程是同时执行的,时间片一般是几十毫秒。而每次切换时,需要保存当前的状态起来,以便能够进行恢复先前状态,而这个切换时非常损耗性能,过于频繁反而无法发挥出多线程编程的优势。
二是
线程安全问题,虽然线程之间可以资源共享方便了设计,但是也会面临数据的安全问题。当多个线程需要对 公有变量 进行写操作时,如果没有进行过同步处理,后一个线程可能会修改掉前一个线程存放的数据,从而使前一个线程的参数被修改,而导致数据紊乱。
2、 线程创建方法
2.1 继承 Thread 类
继承 Thread
类创建线程需要重写父类的 run()
方法,通过创建对象的方式来调用 start()
来开启线程的执行。
-
传送门:[
Thread
](E:\\file\\object file\\全栈项目\\summary\\Object.md) 类详解 -
代码测试
// 继承 Thread 重写 run() 方法 public static class Test extends Thread{ @Override public void run() { try { // 线程休眠 0.1 秒 Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } // 打印当前线程的名 System.out.println(Thread.currentThread().getName() + "线程正在执行"); } }
// 创建线程并执行 public static void main(String[] args) { Test test = new Test(); test.start(); System.out.println(Thread.currentThread().getName() + "线程正在执行"); }
输出结果
main线程正在执行 Thread-0线程正在执行
2.2 实现 Runable
实现 Runable
接口创建线程需要实现接口的 run()
方法,通过创建对象的方式作为参数传给一个Thread
的对象调用 start()
来开启线程。
-
代码测试
// 实现 Runnable 接口并实现其 run() 方法 public static class Test implements Runnable{ @Override public void run() { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "线程正在执行"); } }
public static void main(String[] args) { // 创建线程任务 Test test = new Test(); // 创建线程传入任务 new Thread(test).start(); System.out.println(Thread.currentThread().getName() + "线程正在执行"); }
输出结果
main线程正在执行 Thread-0线程正在执行
2.3 实现 Callable 接口
实现 Callable
接口需要实现 call()
方法,该方法可以返回数据。创建对象作为构造参数传给 FutureTask
对象后,将此 FutureTask
的对象传给一个 Thread
对象并通过 start()
开启线程。返回数据可通过 FutureTask
对象的 get()
方法获得,但是调用此方法主线程会暂停知道数据返回。
-
代码测试
public static class Test implements Callable<Integer>{ // 实现接口方法 @Override public Integer call() throws Exception { System.out.println(Thread.currentThread().getName() + "线程正在执行"); int result = 1; for (int i = 1; i < 10; i++){ result *= i; } // 返回计算的结果 return result; } }
public static void main(String[] args) { Test test = new Test(); FutureTask<Integer> task = new FutureTask<>(test); new Thread(task).start(); try { // get() 方法获取子线程的返回值 System.out.println("子线程返回的数据结果 " + task.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "线程正在执行"); }
输出结果
Thread-0线程正在执行 子线程返回的数据结果 362880 main线程正在执行
3、 线程使用
3.1 线程的中断
线程的结束应该由 本身 进行决定,而不是从外部直接取消线程,这样可能会导致线程内部的一些资源没有得到释放,而一直占用系统的资源。
3.1.1 stop()
从线程的外部强制停止该线程的执行,方法不安全,已经过时。
-
代码测试
// 进行迭代输出次数 public static class Test implements Runnable{ @Override public void run() { int count = 0; while(count < 20) { try { // 每隔一秒进行一次 Thread.sleep(1000); count++; System.out.println("正在进行第" + count + "次累加"); } catch (InterruptedException e) { e.printStackTrace(); } } } }
public static void main(String[] args) { Test test = new Test(); Thread thread = new Thread(test); thread.start(); try { // 主线程休眠两秒 Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("销毁子线程"); // 主线程结束后从外部强制结束子线程 thread.stop(); }
输出结果
// 子线程被强制结束 正在进行第1次累加 销毁子线程
3.1.2 interrupt()
中断此线程。给线程添加中断标记,告诉线程该死了,实际线程是否死亡由开发者进行决定,此方法是从线程内部销毁线程,也就是可以将所有资源进行关闭回收后在进行线程的返回销毁。
-
如果在线程内部不进行销毁线程的处理,调用此方法指挥抛出一个
InterruptedException
的异常,而不会回影响线程的继续执行。 -
代码测试
public static void main(String[] args) { Test test = new Test(); Thread thread = new Thread(test); thread.start(); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("销毁子线程"); // 子线程进行异常捕捉,但是没有做出处理 thread.interrupt(); } public static class Test implements Runnable{ @Override public void run() { int count = 0; while(count < 20) { try { Thread.sleep(1000); count++; System.out.println("正在进行第" + count + "次累加"); } catch (InterruptedException e) { // 捕捉中断异常后没有进行线程的销毁工作 e.printStackTrace(); } } } }
输出结果
正在进行第1次累加 正在进行第2次累加 销毁子线程 // 输出异常后子线程继续执行 sleep interrupted 正在进行第3次累加 正在进行第4次累加 ......
-
在捕捉到异常后进行系统资源的释放,然后销毁线程(一个线程的死亡,也就是
run()
函数执行返回了)。public static class Test implements Runnable{ @Override public void run() { int count = 0; while(count < 20) { try { Thread.sleep(1000); count++; System.out.println("正在进行第" + count + "次累加"); } catch (InterruptedException e) { // 捕捉到中断标记后进行资源释放并进行线程的退出 System.out.println("正在销毁占用资源"); System.out.println("子线程死亡退出"); return; } } } }
输出结果
// 子线程成功退出 正在进行第1次累加 正在进行第2次累加 销毁子线程 正在销毁占用资源 子线程死亡退出
3.2 线程安全问题
3.2.1 问题引出
两个线程在使用 读写 一个公用资源数据时(如果都只是读取数据,将不会出现安全问题),可能会出现一个线程需要的数据被另一个线程修改,然后线程拿到的数据错误,但是没有检查出来。
-
事例
public static void main(String[] args) { Test test = new Test(); // 三个线程同时执行一个任务处理 new Thread(test).start(); new Thread(test).start(); new Thread(test).start(); } public static class Test implements Runnable{ private Integer count = 5; @Override public void run() { // 只有数据大于 0 才会进行执行 while (this.count >= 0) { System.out.println(Thread.currentThread().getName() + "拿到了数据" + this.count); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } this.count--; } } }
结果输出
// 可以看到两个线程同时拿到一个数据进行处理,并且还有一个数据处理小于 0 Thread-1拿到了数据1 Thread-2拿到了数据1 Thread-1拿到了数据-1
3.2.2 同步代码块
将需要 写 数据的代码块 synchronized
进行 加锁,同一个时间段内只有一个线程能访问该数据块,能解决多个线程争抢的问题,但是效率会变慢。synchronized
需要传入一个锁对象,任何对象都可以,但是多个线程之间必须是共用的一把锁,不然各自使用自己的锁没有效果。
-
代码测试
public static void main(String[] args) { Test test = new Test(); new Thread(test).start(); new Thread(test).start(); new Thread(test).start(); } public static class Test implements Runnable{ private Integer count = 5; @Override public void run() { while (true) { // 写数据的区域进行加锁,同一时间段内只有一个线程能够访问 synchronized (this){ if (this.count <= 0) break; System.out.println(Thread.currentThread().getName() + "拿到了数据" + this.count); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } this.count--; } } } }
输出结果
// 线程之间队进行取数据,但是会降低执行效率 Thread-0拿到了数据5 Thread-2拿到了数据4 Thread-2拿到了数据3 Thread-1拿到了数据2 Thread-1拿到了数据1
3.2.3 同步方法
用 synchronized
进行对方法进行修饰,和同步代码块效果一致。
-
代码测试
public static void main(String[] args) { Test test = new Test(); new Thread(test).start(); new Thread(test).start(); new Thread(test).start(); } public static class Test implements Runnable{ private Integer count = 5; @Override public void run() { while (deal() != 0) { } } // 用 synchronized 修饰方法,即为同步代码块 private synchronized int deal() { if (this.count <= 0) return 0; System.out.println(Thread.currentThread().getName() + "拿到了数据" + this.count); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } this
以上是关于java学习---多线程的主要内容,如果未能解决你的问题,请参考以下文章