多线程
多线程是提升程序性能非常重要的一种方式,必须掌握的技术。
使用多线程可以让程序充分利用 CPU 资源。(CPU 的资源是有限的)
多线程是指在一个进程中,多个线程同时执行,这里说的同时执行并不是真正意义的同时执行。
系统会为每个线程分配 CPU 资源,在某个具体的时间段内 CPU 资源会被一个线程占用,在不同的时间短内由不同的线程来占用 CPU 资源,所以多个线程还是交替执行,只不过因为 CPU 运行速度太快,我们感受是在同时执行。
优点
-
系统资源得到更合理的利用。
-
程序设计更加简洁。
-
程序响应更快,运行效率更高。
缺点
-
需要更多的内存空间来支持多线程。
-
多线程并发访问的情况可能会影响数据的准确性。(两个人同时抢票,票的归属问题)
-
数据被多线程共享,可能会出现死锁的情况。
多线程并发-->数据不准确-->线程同步解决-->死锁
进程和线程
什么是进程?
进程就是计算机正在运行的一个独立的应用程序。
(电脑开一个软件运行期间这个软件就是一个进程)
进程是一个动态的概念,当我们启动某个应用的时候,进程就产生了,当我们关闭该应用的时候,进程的生命周期就是我们在使用该软件的整个过程。
什么是线程?
线程是组成进程的基本单位,可以完成特定的功能,一个进程是由一个或多个线程组成的。
应用程序时静态的,进程和线程时动态的,有创建有销毁,存在是暂时的,不是永久的。
进程和线程的区别
进程在运行时拥有独立的内存空间,即每个进程所占用的内存空间都是独立的,互不干扰。
线程是共享内存空间的,但是每个线程的执行都是相互独立的,单独的线程是无法执行的,由进程来控制多个线程的执行。
多线程:
整个程序如果是一条回路,说明程序只有一个线程。
程序有两条回路,同时向下执行,这种情况就是多线程,两个线程同时在执行。
Java 写程序三部分组成:
1、JDK 系统类库
JRE: Java Runtime Environment (Java 运行环境),仅供运行程序的。
JDK: Java Development Kit (Java 开发工具包),如果需要进行程序开发,必须安装 JDK。
String、Scanner、包装类 . . . . . .
java.lang.Thread
javax.servlet.Serviet
2、第三方类库
非 Java 官方的组织提供的一些成熟好用的工具,C3P0 数据库连接池、Spring 框架、DBUtils、Dom4J . . . . . .
github:全球最大的同性交友网站
3、开发者自定义的代码
根据具体的业务需求编写的业务代码。
Java 中线程的使用
Java 中使用线程有两种方式:
-
继承 Thread 类
1.创建自定义类并继承 Thread 类。
2.重写 Thread 类中的 run 方法,并编写该线程的业务逻辑代码。
package com.bsj.demo01;
public class MyThread extends Thread{
@Override//重写
public void run() {
//定义业务逻辑
for (int i = 0; i <10; i++) {
System.out.println("---------MyThread");
}
}
}
3.使用
package com.bsj.demo01;
public class Test {
public static void main(String[] args) {
//main是一个主线程,这里有三个线程
//开启两个子线程
MyThread thread1 = new MyThread();
MyThread2 thread2 = new MyThread2();
thread1.start();
thread2.start();
}
}
注意:不能通过 run 方法来调用线程的任务,因为 run 方法调用相当于普通对象的执行,并不会去抢占 CPU 资源。
只有通过 start 方法才能开启线程, 进而去抢占 CPU 资源,当某个线程抢占到 CPU 资源后,会自动调用 run 方法。
-
实现 Runnable 接口
1、创建自定义类并实现 Runnable 接口。
2、实现 run 方法,编写该线程的业务逻辑代码。
package com.bsj.demo01;
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println("======MyRunnable========");
}
}
}
3、使用。
package com.bsj.demo01;
public class Test {
public static void main(String[] args) {
// //开启两个子线程
// MyThread thread1 = new MyThread();
// MyThread2 thread2 = new MyThread2();
// thread1.start();
// thread2.start();
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
MyRunnable2 myRunnable2 = new MyRunnable2();
Thread thread2 = new Thread(myRunnable2);
thread2.start();
}
}
线程和任务:
线程是去抢占 CPU 资源,任务是具体执行业务逻辑的,线程内部会包含一个任务,线程启动(start),当抢占到资源之后,任务就开始执行(run)。
两种种方式的区别:
-
MyThread 继承 Thread 类的方式,直接在类中重写 run 方法,使用的时候,直接实例化 MyThread,start 即可,因为 Thread 内部存在 Runnable。
-
MyRunnable,实现 Runnable 接口的方法,在实现类中重写 run 方法,使用的时候,需要先创建 Thread 对象,并将 MyRunnable 注入到 Thread 中,Thread.start。(耦合度更低,程序的灵活性和扩展性很好)
实际开发中推荐使用第二种方式。
解耦合就是把东西分开。
线程的状态
线程共有 5 种状态,在特定的情况下,线程可以在不同的状态之间切换,5 种状态如下所示。
-
创建状态:实例化一个新的线程对象,还未启动。
-
就绪状态:创建好的线程对象调用 start 方法完成启动,进入线程池等待抢占 CPU资源。
-
运行状态:线程对象获取 CPU 资源,在一定的时间内执行任务。
-
阻塞状态:正在运行的线程暂停执行任务,释放所占用的 CPU 资源,并在解除阻塞后也不能直接回到运行状态,而是重新回到就绪状态等待获取 CPU 资源。
-
终止状态:线程运行完毕或者因为异常导致该线程终止运行。
线程状态之间的转换图
线程调度
-
线程休眠
让当前的线程暂停执行,从运行状态进入阻塞状态,将 CPU 的资源让给其他线程的调度方式,通过 sleep() 来实现。
sleep(long millis),调用时需要传入休眠时间,单位为 ms 。
package com.bsj.demo2;
public class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if (i == 5) {
try {
sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(i+"----------MyThread");
}
}
}
也可以在类的外部调用 sleep 方法。
MyThread2 myThread2 = new MyThread2();
try {
myThread2.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
myThread2.start();
在外部调用需要注意,休眠一定要放在启动之前。
如何让主线程休眠?直接通过静态方式来调用sleep方法。( Thread.sleep() )
public static void sleep(long millis) throws InterruptedException;
sleep 是静态本地方法,可以通过类调用,也可以通过对象调用,方法定义抛出 InterruptedException,InterruptedException 继承 Exception,外部调用时必须手动处理异常。
-
线程合并
合并是指将指定的某个线程加入到当前线程中,合并为一个线程,由两个线程交替执行变成一个线程中的两个子线程顺序执行。
通过调用 join 方法来实现合并,具体如何合并?
线程甲和线程乙,线程甲执行到某个时间点的时候调用线程乙的 join 方法,则表示从当前时间点开始 CPU 资源被线程乙独占,线程甲进入阻塞状态,直到线程乙执行完毕,线程甲进入就绪状态,等待获取 CPU 资源进入运行状态。
join 方法重载,join() 表示乙线程执行完毕之后才能执行其他线程,join(long millis) 表示乙线程执行 millis 毫秒之后,无论是否执行完毕,其他线程都可以和它争夺 CPU 资源。
package com.bsj.demo2;
public class JoinRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(i+"------------JoinRunnable");
}
}
}
package com.bsj.demo2;
public class JoinTest {
public static void main(String[] args) {
/*
* 两个线程,主线程、join线程
* 主线程的逻辑:当i==10,join线程合并到主线程中
* */
JoinRunnable joinRunnable = new JoinRunnable();
Thread thread = new Thread(joinRunnable);
thread.start();
for (int i = 0; i < 100; i++) {
if (i == 10){
try {
thread.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(i+"main+++++++++");
}
}
}
package com.bsj.demo2;
public class JoinRunnable2 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(i+"----------------JoinRunnable");
}
}
}
package com.bsj.demo2;
public class JoinTest2 {
public static void main(String[] args) {
JoinRunnable2 joinRunnable2 = new JoinRunnable2();
Thread thread = new Thread(joinRunnable2);
thread.start();
for (int i = 0; i < 100; i++) {
if (i == 10){
try {
thread.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println(i+"++++++++++main");
}
}
}
在某个特定的时间点,让线程暂停抢占 CPU 资源的行为,运行状态 -> 阻塞状态
线程礼让
线程礼让是指在某个特定的时间点,让线程暂停抢占 CPU 资源的行为,运行状态 or 就绪状态 -> 阻塞状态,将 CPU 资源让给其他线程来使用。
假如线程甲和线程乙在交替执行,某个时间点线程甲做出了礼让,所以在这个时间节点线程乙拥有了 CPU 资源,执行业务逻辑,但不代表线程甲一直暂停执行。
线程甲只是在特定的时间节点礼让,过了时间节点,线程甲再次进入就绪状态,和线程乙争夺 CPU 资源。
通过 yield 方法实现。
package com.bsj.Demo03;
public class YieldThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if (i == 5){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"-----"+i);
}
}
}
package com.bsj.Demo03;
public class YieldThread2 extends Thread{
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+"======="+i);
}
}
}
package com.bsj.Demo03;
public class Test {
public static void main(String[] args) {
YieldThread yieldThread = new YieldThread();
yieldThread.setName("线程1");
YieldThread2 yieldThread2 = new YieldThread2();
yieldThread2.setName("线程2");
yieldThread.start();
yieldThread2.start();
}
}
线程中断
有很多情况会造成线程停止运行:
-
线程执行完毕自动停止
-
线程执行过程中遇到错误抛出异常并停止
-
线程执行过程中根据需求手动停止
Java 中实现线程中断有如下常用方法:
-
public void stop()
-
public void interrupt()
-
public boolean isInterrupted()
stop 方法在新版本中的 JDK 已经不推荐使用,重点关注后两个方法。
interrupt 是一个实例方法,当一个线程对象调用该方法时,表示中断当前线程对象。每个线程对象都时通过一个标标志位来判断当前是否为中断状态。
isInterrupted 就是用来获取当前线程对象的标志位:true 表示清楚了标志位,当前线程已经中断;false 表示没有清楚标志位,当前对象没有中断。
当一个线程对象处于不同的状态时,中孤断机制也是不同的。
创建状态:实例化线程对象,不启动。
package com.bsj.Demo04;
public class Test {
public static void main(String[] args) {
Thread thread = new Thread();
System.out.println(thread.getState());
thread.interrupt();
System.out.println(thread.isInterrupted());
}
}
NEW 表示当前线程对象为创建状态,false 表示当前线程未中断,因为当前线程没有启动,不存在中断,不需要清楚标志位。
匿名内部类
package com.bsj.Demo04;
public class Test2 {
public static void main(String[] args) {
// MyRunnable myRunnable = new MyRunnable();
// Thread thread = new Thread(myRunnable);
// thread.start();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i+"-----main");
}
}
});
thread.start();
}
}
package com.bsj.Demo04;
public class Test2 {
public static void main(String[] args) {
// MyRunnable myRunnable = new MyRunnable();
// Thread thread = new Thread(myRunnable);
// thread.start();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i+"-----main");
}
}
});
thread.start();
System.out.println(thread.getState());
thread.interrupt();
System.out.println(thread.isInterrupted());
System.out.println(thread.getState());
}
}
线程同步
Java 中允许多线程并行访问,同一时间段内多个线程同时完成各自的操作。
多个线程同时操作同一个共享数据时,可能会导致数据不准确的问题。
使用线程同步可以解决上述问题。
可以通过 synchronized 关键字来修饰方法实现线程同步,每个 Java 对象都有一个内置锁会保护使用 synchronized 关键字修饰的方法,要调用该方法就必须先获得锁,否则就处于阻塞状态。
非线程同步
package com.bsj.Demo05;
public class Account implements Runnable{
private static int num;
@Override
public synchronized void run() {
//1、num++操作
num++;
//2、休眠1毫秒
try {
Thread.currentThread().sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//3、打印输出
System.out.println(Thread.currentThread().getName()+"你当前是第"+num+"位访客");
}
}
线程同步
package com.bsj.Demo05;
public class Account implements Runnable{
private static int num;
@Override
public void run() {
//1、num++操作
num++;
//2、休眠1毫秒
try {
Thread.currentThread().sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//3、打印输出
System.out.println(Thread.currentThread().getName()+"你当前是第"+num+"位访客");
}
}
Test
package com.bsj.Demo05;
public class Test {
public static void main(String[] args) {
Account account = new Account();
Thread t1 = new Thread(account,"张三");
Thread t2 = new Thread(account,"李四");
t1.start();
t2.start();
}
}
synchronized 关键字可以修饰实例方法,也可以修饰静态方法,两者在使用的时候时有区别的。
package com.bsj.Demo05;
public class SynchronizedTest {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
SynchronizedTest.test();
}
});
thread.start();
}
}
/*
* 先输出start...
* 间隔1s
* 再输出end...
* ...
* */
public synchronized static void test(){
//1.输出start
System.out.println("start.......");
//2.休眠
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//3.输出end
System.out.println("end........");
}
}
synchronized 修饰非静态方法
package com.bsj.Demo05;
public class SynchronizedTest2 {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
SynchronizedTest2 synchronizedTest2 = new SynchronizedTest2();
synchronizedTest2.test();
}
});
thread.start();
}
}
public synchronized void test(){
System.out.println("start......");
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("end......");
}
}
给实例方法(非静态方法)添加 synchroniez 关键字并不能实现线程同步。
线程同步的本质是锁定多个线程所共享的资源,synchronized 还可以修饰代码块,会为代码块加上内置锁,从而实现同步。
package com.bsj.Demo05;
public class SynchronizedTest3 {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
SynchronizedTest3.test();
}
});
thread.start();
}
}
public static void test(){
synchronized (SynchronizedTest3.class){
System.out.println("start...");
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("end...");
}
}
}
如何判断线程同步或是不同步?
找到关键点:锁定的资源在内存中是一份还是多份?一份大家需要排队,线程同步;多份(一人一份),现场不同步。
无论是锁定方法还是锁定对象,锁定类,只需要分析这个方法、对象、类在内存中有几份即可。
-
对象一般都是多份
-
类一定是一份
-
方法就看是静态方法还是非静态方法,静态方法一定是一份,非静态方法一般是多份
线程安全的单例模式
单例模式是一种常见的软件设计模式,核心思想是一个类只有一个实例对象。
JVM:栈内存、堆内存(变量:基本数据类型直接存储到栈内存;引用类型:需要栈内存和堆内存结合起来进行存储,栈内存存储的是地址),栈里面只能是数值。
单线程模式下的单例模式:
package com.bsj.Demo07;
public class SingletonDemo {
private static SingletonDemo singletonDemo;
private SingletonDemo() {
System.out.println("创建了SingletonDemo...");
}
public static SingletonDemo getInstance() {
if (singletonDemo == null){
singletonDemo = new SingletonDemo();
}
return singletonDemo;
}
}
package com.bsj.Demo07;
public class Test {
public static void main(String[] args) {
SingletonDemo singletonDemo = SingletonDemo.getInstance();
SingletonDemo singletonDemo1 = SingletonDemo.getInstance();
SingletonDemo singletonDemo2 = SingletonDemo.getInstance();
System.out.println(singletonDemo1 == singletonDemo2);
System.out.println(singletonDemo == singletonDemo1);
}
}
多线程模式下的单例模式
package com.bsj.Demo07;
public class SingletonDemo {
private static SingletonDemo singletonDemo;
private SingletonDemo() {
System.out.println("创建了SingletonDemo...");
}
public synchronized static SingletonDemo getInstance() {
if (singletonDemo == null){
singletonDemo = new SingletonDemo();
}
return singletonDemo;
}
}
package com.bsj.Demo07;
public class Test2 {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
SingletonDemo singletonDemo = SingletonDemo.getInstance();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
SingletonDemo singletonDemo = SingletonDemo.getInstance();
}
}).start();
}
}
双重检测,synchronized 修饰代码块.
-
线程同步是为了实现线程安全,如果只创建一个对象,那么线程就是安全的。
-
如果 synchronized 锁定的是多个线程共享的数据(同一个对象),那么线程就是安全的。
package com.bsj.Demo07;
import java.awt.print.PrinterGraphics;
public class SingletonDemo {
private volatile static SingletonDemo singletonDemo;
private SingletonDemo() {
System.out.println("创建了SingletonDemo...");
}
public static SingletonDemo getInstance() {
if(singletonDemo == null){
synchronized(SingletonDemo.class) {
if (singletonDemo == null){
singletonDemo = new SingletonDemo();
}
}
}
return singletonDemo;
}
}
volatile 的作用是可以使内存中的数据对象线程可见。
主内存对线程是不可见的,添加 volatile 关键字之后,主内存对线程可见。
死锁 DeadLock
前提:一个线程完成业务需要同时访问两个资源。
死锁:多个线程同时在完成业务,出现争抢资源的情况。
资源类:
package com.bsj.Demo08;
public class DeadLockRunnable implements Runnable{
//编号
public int num;
//资源
private static Chopsticks chopsticks1 = new Chopsticks();
private static Chopsticks chopsticks2 = new Chopsticks();
/*
* num = 1拿到 chopsticks1,等待 chopsticks2
* num = 2拿到 chopsticks2,等待 chopsticks1
* */
@Override
public void run() {
if(num == 1){
System.out.println(Thread.currentThread().getName()+"拿到了chopsticks1,等待获取chopsticks2");
synchronized (chopsticks1){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (chopsticks2){
System.out.println(Thread.currentThread().getName()+"用餐完毕!");
}
}
}
if (num == 2){
System.out.println(Thread.currentThread().getName()+"拿到chopsticks2,等待获取chopsticks1");
synchronized (chopsticks2){
synchronized (chopsticks1){
System.out.println(Thread.currentThread().getName()+"用餐完毕!");
}
}
}
}
}
package com.bsj.Demo08;
public class Chopsticks {
}
package com.bsj.Demo08;
public class DeadLockTest {
public static void main(String[] args) {
DeadLockRunnable deadLockRunnable1 = new DeadLockRunnable();
deadLockRunnable1.num = 1;
DeadLockRunnable deadLockRunnable2 = new DeadLockRunnable();
deadLockRunnable2.num = 2;
new Thread(deadLockRunnable1,"张三").start();
new Thread(deadLockRunnable2,"李四").start();
}
}
如何破解死锁
不要让多线程并发访问
package com.bsj.Demo08;
public class DeadLockTest {
public static void main(String[] args) {
DeadLockRunnable deadLockRunnable1 = new DeadLockRunnable();
deadLockRunnable1.num = 1;
DeadLockRunnable deadLockRunnable2 = new DeadLockRunnable();
deadLockRunnable2.num = 2;
new Thread(deadLockRunnable1,"张三").start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
new Thread(deadLockRunnable2,"李四").start();
}
}
使用 lamdba 表达式简化代码开发
package com.bsj.Demo09;
public class Test3 {
public static void main(String[] args) {
//lambda表达式
new Thread(()->{
for (int i = 0; i < 100; i++) {
System.out.println("+++++++Runnable");
}
}).start();
}
}
package com.bsj.Demo09;
public class Test3 {
public static void main(String[] args) {
//lambda表达式
new Thread(() -> {for (int i = 0; i < 100; i++) System.out.println("++++++++++Runnable");}).start();}
}
总结
Java多线程是Java语言中一个非常重要的特性,它可以让程序在同时执行多个任务的同时提高程序的运行效率和性能。以下是Java多线程的一些总结:
-
Java中的多线程可以通过继承Thread类或实现Runnable接口来创建线程。
-
synchronized关键字可以用来确保线程安全,避免多个线程同时访问共享资源的问题。
-
wait()和notify()方法可以用来实现线程的等待和唤醒,wait()方法会使线程进入等待状态,而notify()方法会唤醒一个正在等待的线程。
-
join()方法可以用来实现线程的等待,即等待一个线程的结束后再执行其他的操作。
-
线程池是一种提高程序性能的方式,它可以减少线程的创建和销毁的开销,从而提高程序的运行效率。
-
Java中的锁机制可以用来控制线程的访问,避免线程间的相互干扰,例如ReentrantLock和ReadWriteLock等。
-
Java中的线程管理API可以用来管理线程,例如ThreadLocal类可以让每个线程都拥有自己的变量副本,避免线程间的共享问题。
-
Java中的并发集合类可以用来实现多线程之间的数据共享,例如ConcurrentHashMap和ConcurrentLinkedQueue等。
-
多线程编程需要注意一些常见的问题,例如死锁、线程安全、线程间通信等。
总之,Java多线程是Java编程中非常重要的一个特性,掌握Java中的多线程编程可以让程序在同时执行多个任务的同时提高程序的运行效率和性能。