多线程
并发和并行
- 并行:指两个或多个事件同一时刻发生(同时执行)
- 并发:指两个或多个事件同一时间段内发生(交替执行)
线程和进程
- 进程:进入到内存中的程序就是进程
- 线程:在某一个应用程序运行某一个功能时,系统会为该功能开启一个通往cpu的通道,cup通过这个通道可以执行该功能。这个通道就是线程。
线程调度
分时调度
所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间。
抢占式调度
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
Thread类
线程开启我们需要用到了 java.lang.Thread 类。
构造方法:
- public Thread() :分配一个新的线程对象。
- public Thread(String name) :分配一个指定名字的新的线程对象。
- public Thread(Runnable target) :分配一个带有指定目标新的线程对象。
public Thread(Runnable target,String name) :分配一个带有指定目标新的线程对象并指定名字。
常用方法:
- public String getName() :获取当前线程名称。
- public void start() :导致此线程开始执行; Java虚拟机调用此线程的run方法。
- public void run() :此线程要执行的任务在此处定义代码。
- public static void sleep(long millis) :使当前正在执行的线程以指定的毫秒数暂停(暂时停止执行)。
public static Thread currentThread() :返回对当前正在执行的线程对象的引用。
创建线程的方式
继承Thread类
步骤如下
1.创建一个类,继承Thread类
2.重写run方法–>设置线程任务(线程类中要干的事情)
3.创建Thread的子类对象
4.调用Thread中的Start()(使该线程开始执行),java虚拟机会自动调用该线程的run方法
//1.创建一个类,继承Thread类
public class Thread01 extends Thread {
//2.重写run方法–>设置线程任务(线程类中要干的事情)
@Override
public void run(){
for (int i = 0; i < 10; i++) {
try {
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"执行了"+i);
}
}
}
public static void main(String[] args) {
//3.创建Thread的子类对象
Thread01 thread01 = new Thread01();
// 4.调用Thread中的Start()(使该线程开始执行),java虚拟机会自动调用该线程的run方法
thread01.start();
}
注意
一个线程对象不能连续调用start()
java程序属于抢占式调度,cup会在多个线程之间做高速切换,会导致每一次运行结果都不一致。
实现Runnable接口方式演示
步骤如下:
1. 定义Runnable接口的实现类。
2. 重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
3. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象。
4. 调用线程对象的start()方法来启动线程。
// 1. 定义Runnable接口的实现类。
public class Thread02 implements Runnable {
//2. 重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"执行了"+i);
}
}
}
public static void main(String[] args) {
//3. 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正的线程对象
Thread02 thread02 = new Thread02();
Thread thread = new Thread(thread02);
//4. 调用线程对象的start()方法来启动线程。
thread.start();
}
创建多线程程序两种方式的区别
- 使用Runnable接口的方式实现多线程程序,可以避免单继承的局限性。
- 使用Runnable接口的方式实现多线程程序,可以把设置线程任务和开启线程进行解耦。
匿名内部类形式创建多线程
好处
使用线程的内匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作。
步骤
使用匿名内部类的方式实现Runnable接口,重写Runnable接口中的run方法
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName()+i);
}
}
}).start();
}
高并发及线程安全
高并发:
是指在某个时间点上,有大量的用户(线程)同时访问同一资源。例如:天猫的双11购物节、12306的
在线购票在某个时间点上,都会面临大量用户同时抢购同一件商品/车票的情况。
线程安全:
在某个时间点上,当大量用户(线程)访问同一资源时,由于多线程运行机制的原因,可能会导致被访问的资源出现"数据污染"的问题。
多线程的安全性问题
可见性
启动一个线程,在线程中将一个变量的值更改,而主线程却一直无法获得此变量的新值
//线程类
public class Thread04_KeJianXing implements Runnable {
public static int a = 0;
@Override
public void run() {
System.out.println("先睡两秒再说!");
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("醒了!改变一下a的值!");
a = 1;
System.out.println("修改执行完毕!");
}
}
//测试类
public static void main(String[] args) {
Thread04_KeJianXing t = new Thread04_KeJianXing();
Thread thread = new Thread(t);
thread.start();
while(true){
if(Thread04_KeJianXing.a == 1){
System.out.println("main线程执行完毕");
break;
}
}
}
运行结果
循环没有停止!说明没有读取到改变后的a值。
有序性
有些时候“编译器”在编译代码时,会对代码进行“重排”,例如:
int a = 10; //1
int b = 20; //2
int c = a + b; //3
第一行和第二行可能会被“重排”:可能先编译第二行,再编译第一行,总之在执行第三行之前,会将1,2编译完毕。1和2先编译谁,不影响第三行的结果。
但在“多线程”情况下,代码重排,可能会对另一个线程访问的结果产生影响
原子性
//线程类
public class MyThread extends Thread {
public static int a = 0;
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
a++;
}
System.out.println("修改完毕!");
}
}
//测试类
public class Demo {
public static void main(String[] args) throws InterruptedException {
//1.启动两个线程
MyThread t1 = new MyThread();
MyThread t2 = new MyThread();
t1.start();
t2.start();
Thread.sleep(1000);
System.out.println("获取a最终值:" + MyThread.a);//总是不准确的。原因:两个线程访问a的步骤不具有:原子性
}
}
原因:两个线程访问同一个变量a的代码不具有"原子性"
1. 线程t1先读取a 的值为:0
2. t1被暂停
3. 线程t2读取a的值为:0
4. t2将a = 1
5. t2将a写回主内存
6. t1将a = 1
7. t1将a写回主内存(将t2更改的1,又更改为1)
所以两次加1,但结果仍为1,少加了一次。
volatile关键字
概念
- volatile是一个"变量修饰符",它只能修饰"成员变量",它能强制线程每次从主内存获取值,并能保证此变量不
- 会被编译器优化。
- volatile能解决变量的可见性、有序性;
- volatile不能解决变量的原子性
volatile解决可见性
//线程类
public class Thread04_KeJianXing implements Runnable {
public volatile static int a = 0;//增加volatile关键字
@Override
public void run() {
System.out.println("先睡两秒再说!");
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("醒了!改变一下a的值!");
a = 1;
System.out.println("修改执行完毕!");
}
}
//测试类
public static void main(String[] args) {
Thread04_KeJianXing t = new Thread04_KeJianXing();
Thread thread = new Thread(t);
thread.start();
while(true){
if(Thread04_KeJianXing.a == 1){
System.out.println("main线程执行完毕");
break;
}
}
}
运行结果
当变量被修饰为volatile时,会迫使线程每次使用此变量,都会去主内存获取,保证其可见性
volatile解决有序性
当变量被修饰为volatile时,会禁止代码重排
volatile不能解决原子性
…
---------------------
作者:次奥QNMLGB
来源:CSDN
原文:https://blog.csdn.net/weixin_44564242/article/details/104999588?depth_1-utm_source=distribute.pc_category.none-task&request_id=&utm_source=distribute.pc_category.none-task
版权声明:本文为作者原创文章,转载请附上博文链接!