现在的操作系统是多任务操作系统。多线程是实现多任务的一种方式。进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程中可以启动多个线程。比如在Windows系统中,一个运行的exe就是一个进程。线程是指进程中的一个执行流程,一个进程中可以运行多个线程。比如java.exe进程中可以运行很多线程。线程总是属于某个进程,进程中的多个线程共享进程的内存。“同时”执行是人的感觉,在线程之间实际上轮换执行。
一、创建线程
1、继承Thread
public class MyThread extends Thread{
@Override
public void run() {
super.run();
System.out.println("我是线程--->"+Thread.currentThread().getName());
}
}
@Test
public void test(){
MyThread myThread=new MyThread();
MyThread myThread1=new MyThread();
myThread.start();
myThread1.start();
}
执行结果:
我是线程--->Thread-1
我是线程--->Thread-0
2、实现runnable接口
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("我是线程--->" + Thread.currentThread().getName());
}
}
@Test
public void test1() {
MyRunnable myRunnable = new MyRunnable();
Thread thread=new Thread(myRunnable);
Thread thread1=new Thread(myRunnable);
thread.start();
thread1.start();
}
执行结果:
我是线程--->Thread-1
我是线程--->Thread-0
3、Callable创建线程
Callable 、Future创建带返回值的线程
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<String> future = executorService.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return "通过Callable方式创建线程的返回结果";
}
});
try {
String res = future.get();
System.out.println(res);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
二、多线程互斥
1、互斥
同一个资源类被多线程操作,临界资源在某一时刻被多个线程访问,会造成数据脏读,因此 多线程互斥技术势在必行,以经典的银行取款为例。
public class Account {
private SimpleDateFormat sdf=new SimpleDateFormat("YYYY-MM-DD HH:mm:ss");
///账户余额
private int account=1000;
public void take(int m) {
account-=m;
System.out.println(getTime()+"********取款:"+m+"元");
}
public void save(int m) {
account+=m;
System.out.println(getTime()+"--------存款:"+m+"元");
}
public void getAccount() {
System.out.println(getTime()+"########余额:"+account+"元");
}
String getTime() {
return sdf.format(new Date());
}
}
模拟多线程:
public static void main(String[] args) {
account = new Account();
for (int i = 0; i < 3; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.take(100);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.save(100);
account.getAccount();
}
}).start();
}
}
看看执行结果,余额度尽然变成900了
2019-01-25 17:13:08********取款:100元
2019-01-25 17:13:08********取款:100元
2019-01-25 17:13:08--------存款:100元
2019-01-25 17:13:08########余额:900元
2019-01-25 17:13:08--------存款:100元
2019-01-25 17:13:08########余额:900元
2019-01-25 17:13:08********取款:100元
2019-01-25 17:13:08--------存款:100元
2019-01-25 17:13:08########余额:900元
为了解决多线程下共享数据脏读问题,java提供了线程锁。
2、线程锁
(1)锁的分类: 自旋锁、阻塞锁、可重入锁、读写锁、互斥锁、悲观锁、乐观锁、公平锁、偏向锁 对象锁、线程锁、锁粗化、锁消除、轻量级锁、重量级锁、独享锁、分段锁。
(2)java常见的锁有:Synchronized 、ReentrantLock 、 ReentrantReadWriteLock。
Synchronized是一个非公平,悲观,独享,互斥,可重入的重量级锁。ReentrantLock 是一个默认非公平但可实现公平的,悲观,独享,互斥,可重入轻量级锁。ReentrantReadWriteLocK是一个,默认非公平但可实现公平的,悲观,写独享,读共享,读写,可重入的轻量级锁。
3、synchronized
使用synchronized来体验一下互斥效果
private static Account account;
public static void main(String[] args) {
account = new Account();
for (int i = 0; i < 3; i++) {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (account) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.take(100);
account.getAccount();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (account) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
account.save(100);
account.getAccount();
}
}
}).start();
}
}
打印结果
2019-01-25 17:30:43********取款:100元
2019-01-25 17:30:43########余额:900元
2019-01-25 17:30:44********取款:100元
2019-01-25 17:30:44########余额:800元
2019-01-25 17:30:45--------存款:100元
2019-01-25 17:30:45########余额:900元
2019-01-25 17:30:46--------存款:100元
2019-01-25 17:30:46########余额:1000元
2019-01-25 17:30:47********取款:100元
2019-01-25 17:30:47########余额:900元
2019-01-25 17:30:48--------存款:100元
2019-01-25 17:30:48########余额:1000元
三、多线程通信
多线程通信有很多方式,如Object的wait/notify、condition的 await/signal 、LockSupport的park/unpark,本文以Object自带的wait和notify方法实现线程间的通信。
现一个需求,子线程循环20次,主线程循环10次,子线程执行2次,主线程执行1次,按顺序交替执行
public class ThreadTest {
public static void main(String[] args) {
ThreadSubMain threadSubMain = new ThreadSubMain();
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
threadSubMain.subExcute(i);
}
}
}).start();
threadSubMain.mainExcute();
}
static class ThreadSubMain {
private boolean isWait = true;
/// 子线程循环20次
synchronized void subExcute(int i) {
while (!isWait) {
try {
this.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
for (int j = 0; j < 2; j++) {
System.out.println("第" + i + "个,第" + j + "次子线程--->" + Thread.currentThread().getName());
}
//当子线程执行两次后,改变标识符,唤醒主线程执行;
isWait = false;
this.notify();
}
/// 主线程循环10次
synchronized void mainExcute() {
for (int i = 0; i < 10; i++) {
while (isWait) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("第" + i + "个主线程--->" + Thread.currentThread().getName());
}
//当主线程执行一次后,改变标识符,唤醒主子线程
isWait = true;
this.notify();
}
}
}
}
第0个,第0次子线程--->Thread-0
第0个,第1次子线程--->Thread-0
第0个主线程--->main
第1个,第0次子线程--->Thread-0
第1个,第1次子线程--->Thread-0
第1个主线程--->main
第2个,第0次子线程--->Thread-0
第2个,第1次子线程--->Thread-0
第2个主线程--->main
第3个,第0次子线程--->Thread-0
第3个,第1次子线程--->Thread-0
第3个主线程--->main
第4个,第0次子线程--->Thread-0
第4个,第1次子线程--->Thread-0
第4个主线程--->main
第5个,第0次子线程--->Thread-0
第5个,第1次子线程--->Thread-0
第5个主线程--->main
第6个,第0次子线程--->Thread-0
第6个,第1次子线程--->Thread-0
第6个主线程--->main
第7个,第0次子线程--->Thread-0
第7个,第1次子线程--->Thread-0
第7个主线程--->main
第8个,第0次子线程--->Thread-0
第8个,第1次子线程--->Thread-0
第8个主线程--->main
第9个,第0次子线程--->Thread-0
第9个,第1次子线程--->Thread-0
第9个主线程--->main
四、线程中断
Thread类中有3个关于中断的方法,成员方法interrupt和isInterrupted,以及静态方法interrupted。下面简要的介绍一下这三个方法。
1、interrupt
给当前线程设置中断标志位,不会立即中断线程。如果当前线程正在中断,则设置标志位可以成功,否会调用checkAcess方法抛出SecurityException异常,比如当前线程阻塞调用wait 、join、sleep方法,中断标志位将被清除,中断不活动的线程也是无效的。
2、isInterrupted
获取当前线程的中断状态。
Thread thread = Thread.currentThread();
System.out.println("1---->"+thread.getName()+" " + thread.isInterrupted());
thread.interrupt();
System.out.println("2---->"+thread.getName()+" " + thread.isInterrupted());
1---->main false
2---->main true
3、interrupted
检查当前线程的中断标志,测试当前线程是否被中断,如果当前线程设置了中断标志,则返回一个boolean并清除中断状态,第二次调用的时候中断状态已经清除并返回false。
System.out.println("1---->"+Thread.currentThread().getName()+" " + Thread.interrupted());
System.out.println("2---->"+Thread.currentThread().getName()+" " + Thread.interrupted());
Thread.currentThread().interrupt();
System.out.println("3---->"+Thread.currentThread().getName()+" " + Thread.interrupted());
System.out.println("4---->"+Thread.currentThread().getName()+" " + Thread.interrupted());
1---->main false
2---->main false
3---->main true
4---->main false
由运行结果可以看出,和结论完全吻合。
五、内存模型JMM
Java(Java Memory Model,JMM)是Java虚拟机规范的一部分,也就是说JMM本质是一系列的规范,这些规范用于定义Java程序中多线程之间共享内存的行为。它描述了变量(包括实例字段、静态字段和数组元素)在内存中的存储和读取方式,以及在多线程环境中如何确保可见性和有序性。
共享变量:在多个线程之间可见的变量,例如对象的字段、静态变量等;
主内存:是线程间共享的内存区域所有线程都可以访问。主内存存储了共享变量的原始副本;
工作内存:是线程私有的内存区域,每个线程有自己的工作内存。工作内存中存储了主内存中共享变量的副本。
借用一下网上先贤总结好的图如下
1、JMM的作用
保证多线程运行代码执行的原子性、可见性、有序性。
★ 原子性:
确保在多线程环境中的执行结果与在单线程环境中的执行结果一致。一个操作一气呵成,要么完整地执行,要么不执行,不会出现部分执行的情况。
在多线程编程中,使用 synchronized 关键字可以确保代码块或方法在同一时刻只能被一个线程执行,避免多线程并发访问导致的数据竞争问题,从而保证操作的原子性。另外CAS通过调用native方法操作机器码也可以保证原子性。
★ 内存可见性:
可见性指的是一个线程对共享变量的修改能够及时对其他线程可见。
★有序性:
有序性指的是程序执行的顺序与代码的顺序一致。
2、Happens-Before原则
先行(Happens-Before)原则规定了一系列操作之间的先后顺序关系。如果一个操作Happens-Before于另一个操作,那么前一个操作对于后一个操作的结果是可见的。
★ 程序顺序规则: 在一个线程内,按照程序代码的先后顺序执行的操作,会产生Happens-Before的关系。
★监视锁定规则: 对于一个锁的解锁操作Happens-Before于后续对于该锁的加锁操作。
★ volatile变量规则: 对一个volatile变量的写操作Happens-Before于后续对这个变量的读操作。
★ 传递性规则: 如果操作A Happens-Before操作B,操作B Happens-Before操作C,则操作A Happens-Before操作C。
★ 线程启动规则:线程的启动操作Happens-Before于新线程的所有操作。
★ 线程终止规则:线程的所有操作Happens-Before于其他线程检测到该线程终止。
对于程序员来说,编写Java的多线程程序,只要按照happens-before
规则来,就能保证变量的可见性和有序性。
3、volatile
关键字
volatile
关键字修饰变量,保证变量的修改对其他线程是可见的,防止编译器和处理器对指令进行重排序优化。在多线程编程中使用volatile
可以保证可见性和有序性。
JMM通过内存屏障(memory barrier)来禁止重排序的。对于即时编译器来说,它会针对前面提到的每一个
关系,向正在编译的目标方法中插入相应的读读、读写、写读以及写写内存屏障。happens-before
在访问olatile 修饰的变量时,所插入的内存屏障,将禁止 volatile 字段
之前的「内存访问被写操作
至其之后;也将不允许 volatile 字段重排序
之后的内存访问被读操作
至其之前。重排序