一、多线程的基础概念
首先要知道什么是进程,什么是线程。
进程:正在运行的程序,当一个程序进入内存运行,就是一个进程。
线程:包含在进程中,一个进程至少由1个线程组成。
多线程
需要知道2个概念,串行和并行。以下是图例
串行:线程依次执行,A执行完了,B执行,然后再C执行
并行:A、B、C同时执行
并发产生的问题:
- 线程安全问题:多个线程同时操作同一个变量时,会出现数据不准确问题,例如,对象A银行卡账户有100元,线程A和线程B同时对其操作,线程A操作取出了10元,对象A余额还有90;线程B存入20元,余额还有120了,这就出现了数据不一致的问题。
如何避免线程安全问题:
- 同步代码块
- 同步方法
- lock锁
二、如何实现多线程?
在java中,实现多线程有3个方法
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
1、继承Thread类
package com.wenan.stdthread;
/**
* 描述: 继承Thread类,实现一个自定义线程
*/
public class Mythread extends Thread{
private String name;
Mythread(String name) {
this.name = name;
}
@Override
public void run(){
for (int i = 0; i < 100; i++) {
System.out.println(name+"正在运行"+i);
try {
// 随机暂停时间,以查看差异
sleep((int)Math.random()*100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
测试
public class ThreadDemo {
public static void main(String[] args) {
Mythread thread1 = new Mythread("thread1");
Mythread thread2 = new Mythread("thread2");
thread1.start();
thread2.start();
}
}
运行结果
···
thread2正在运行7
thread1正在运行15
thread2正在运行8
thread1正在运行16
thread2正在运行9
···
在线程内部随机暂停时间,可以发现,线程1和线程2是交替运行。
2、实现Runnable接口
package com.wenan.stdthread;
/**
* 描述: MyRunnable
*/
public class MyRunnable implements Runnable {
private String name;
MyRunnable(String name) {
this.name = name;
}
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(name + "正在运行" + i);
try {
// 随机暂停时间,以查看差异
Thread.sleep((int) (Math.random() * 100));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
测试
public class ThreadDemo {
public static void main(String[] args) {
// 通过继承Thread类实现自定义线程
Mythread thread1 = new Mythread("thread1");
Mythread thread2 = new Mythread("thread2");
thread1.start();
thread2.start();
// 通过实现Runnable方法实现多线程
MyRunnable thread3 = new MyRunnable("thread3");
MyRunnable thread4 = new MyRunnable("thread4");
new Thread(thread3).start();
new Thread(thread4).start();
}
}
结果
···
thread4正在运行26
thread2正在运行32
thread3正在运行30
thread3正在运行31
thread1正在运行35
thread4正在运行27
thread2正在运行33
···
3、通过Thread实现和Runnable实现的区别
Thread类实际上也是实现了Runnable接口的类。
在启动的多线程的时候,需要先通过Thread类的构造方法Thread(Runnable target) 构造出对象,然后调用Thread对象的start()方法来运行多线程代码。
实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是扩展Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的。
因此Thread和Runnable的主要区别就是:
- Runnable是一个接口,Thread是一个实现了Runnable接口的类。
- 通过Thread实现的自定义run方法,每次执行时都需要新建一个线程对象,不方便使用,而通过实现Runnable接口的run方法,可以重复在多个线程使用
三、解决线程安全问题
- synchronized
格式:
synchronized (锁对象) {
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
或者
修饰符 synchronized 返回值类型 方法名称(参数列表){
可能会出现线程安全问题的代码(访问了共享数据的代码)
}
先看一个线程不安全的情况
public class MyThread implements Runnable {
// 线程共享的对象
private Integer money = 100;
// 对象名称
private String name;
MyThread(String name) {
this.name = name;
}
@Override
public void run() {
for (int i = 0; i < 10; i++) {
if (money > 0) {
money = money - 10;
System.out.println(name+"取出了10元,还剩"+money+"元");
}
}
}
}
测试
public class sychronizedDemo {
public static void main(String[] args) {
MyThread wangxiaoer = new MyThread("王小二");
new Thread(wangxiaoer).start();
new Thread(wangxiaoer).start();
new Thread(wangxiaoer).start();
}
}
输出结果
王小二取出了10元,还剩80元
王小二取出了10元,还剩80元
王小二取出了10元,还剩60元
王小二取出了10元,还剩70元
王小二取出了10元,还剩40元
王小二取出了10元,还剩30元
王小二取出了10元,还剩20元
王小二取出了10元,还剩10元
王小二取出了10元,还剩50元
王小二取出了10元,还剩0元
上面可以看到,王小二第一次取钱的时候,只取了10元,但钱包只剩下80元,第二次又取了10元,钱包还有 80元。这就产生了数据不一致的问题。
- 使用synchronized对方法进行同步
public class MyThread implements Runnable {
// 线程共享的对象
private Integer money = 100;
// 对象名称
private String name;
MyThread(String name) {
this.name = name;
}
@Override
public /*synchronized*/ void run() {
synchronized(/*this*/Mythread.class) {
for (int i = 0; i < 4; i++) {
if (money > 0) {
int take=10;
money = money - take;
System.out.println(Thread.currentThread().getName()+name + "取出了"+take+"元,还剩" + money + "元");
}
}
}
}
public static void main(String[] args) {
MyThread wangxiaoer = new MyThread("王小二");
new Thread(wangxiaoer).start();
new Thread(wangxiaoer).start();
new Thread(wangxiaoer).start();
}
}
执行结果,线程逐次执行
Thread-0王小二取出了10元,还剩90元
Thread-0王小二取出了10元,还剩80元
Thread-0王小二取出了10元,还剩70元
Thread-0王小二取出了10元,还剩60元
Thread-2王小二取出了10元,还剩50元
Thread-2王小二取出了10元,还剩40元
Thread-2王小二取出了10元,还剩30元
Thread-2王小二取出了10元,还剩20元
Thread-1王小二取出了10元,还剩10元
Thread-1王小二取出了10元,还剩0元
- 使用lock进行加锁
public class Mythread2 implements Runnable {
Lock lock = new ReentrantLock();
// 线程共享的对象
private Integer money = 100;
// 对象名称
private String name;
Mythread2(String name) {
this.name = name;
}
public static void main(String[] args) {
Mythread2 wangxiaoer = new Mythread2("王小二");
new Thread(wangxiaoer).start();
new Thread(wangxiaoer).start();
new Thread(wangxiaoer).start();
}
@Override
public void run() {
for (int i = 0; i < 40; i++) {
lock.lock();
try {
if (money > 0) {
int take = 10;
money = money - take;
System.out.println(Thread.currentThread().getName()+"第"+i+"次执行:" + name + "取出了" + take + "元,还剩" + money + "元");
}else {
int take = 5;
money = money + take;
System.out.println(Thread.currentThread().getName()+"第"+i+"次执行:" +name + "存入了" + take + "元,还剩" + money + "元");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
}
输出结果
Thread-0第0次执行:王小二取出了10元,还剩90元
Thread-0第1次执行:王小二取出了10元,还剩80元
Thread-0第2次执行:王小二取出了10元,还剩70元
Thread-0第3次执行:王小二取出了10元,还剩60元
Thread-0第4次执行:王小二取出了10元,还剩50元
Thread-0第5次执行:王小二取出了10元,还剩40元
Thread-0第6次执行:王小二取出了10元,还剩30元
Thread-0第7次执行:王小二取出了10元,还剩20元
Thread-0第8次执行:王小二取出了10元,还剩10元
Thread-0第9次执行:王小二取出了10元,还剩0元
Thread-0第10次执行:王小二存入了5元,还剩5元
Thread-0第11次执行:王小二取出了10元,还剩-5元
Thread-0第12次执行:王小二存入了5元,还剩0元
Thread-0第13次执行:王小二存入了5元,还剩5元
。。。
- Lock与synchronized 的区别
维度 | synchronize | lock |
---|---|---|
存在层次上 | Java的关键字,在jvm层面上 | 是一个接口 |
锁的释放 | 1、获取锁的线程执行完,自动释放锁 2、线程发生异常 | 在finally中必须释放锁,不然容易造成线程死锁 |
锁的获取 | 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 | 分情况而定,Lock有多个锁获取的方式,大致就是可以尝试获得锁,线程可以不用一直等待(可以通过tryLock判断有没有锁) |
死锁的产生 | 在发生异常时候会自动释放占有的锁,因此不会出现死锁 | 发生异常时候,不会主动释放占有的锁,必须手动unlock来释放锁,可能引起死锁的发生 |
锁的状态 | 无法判断 | 可以判断,通过tryLock判断 |
锁的类型 | 可重入 不可中断 非公平 | 可重入 可判断 可公平(两者皆可) |
性能 | 少量同步 | 大量同步 |
锁的调度 | 使用Object对象本身的wait 、notify、notifyAll调度机制 | 可以使用Condition进行线程之间的调度 |
用法 | 在需要同步的对象中加入此控制,synchronized可以加在方法上,也可以加在特定代码块中,括号中表示需要锁的对象。 | 一般使用ReentrantLock类做为锁。在加锁和解锁处需要通过lock()和unlock()显示指出。所以一般会在finally块中写unlock()以防死锁。 |
四、线程的生命周期和5种状态
- 新建状态(New):在new Thread()的时候,就新建了一个线程。
- 就绪状态(Runnable):调用start()方法时,线程进入就绪状态,等待cpu调度,cpu调度就进入了运行状态。
- ** 运行状态(Running)**:当CPU开始调度处于就绪状态的线程时,此时线程才得以真正执行,即进入到运行状态。注:就绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
- 阻塞状态(Blocked):处于运行状态的线程出于某种原因而暂时放弃CPU使用权,进入阻塞状态。
(一)、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
(三)、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁) - 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期。
五、线程的优先级
每一个 Java 线程都有一个优先级,这样有助于操作系统确定线程的调度顺序。取值范围是1~10;
- Thread.MAX_PRIORITY :优先级是10
- Thread.NORM_PRIORITY:优先级是5
- Thread.MIN_PRIORITY:优先级是1