1 多线程
多线程:是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能
1.1 并发与并行
- 并行:指两个或多个事件在同一时刻发生(同时执行)。
- 并发:指两个或多个事件在同一个时间段内发生(交替执行)。
1.2 线程与进程
- 进程:是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。
- 线程:是进程中的一个执行单元,负责当前进程中程序的执行,一个进程中至少有一个线程。一个进程中是可以有多个线程的,这个应用程序也可以称之为多线程程序。
进程与线程的区别
- 进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。
- 线程:堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多。
线程调度:
- 分时调度:所有线程轮流使用 CPU使用权,平均分配每个线程占用CPU的时间.
- 抢占式调度:
优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个(线程随机性),Java使用的为抢占式调度。
1.3 Thread类
Thread作为线程开启,在线程的调度中必须要用到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类方式
- 是实现Runnable接口方式
1.4 创建线程方式一_继承方式
步骤:
- 定义Thread类的子类,并重写该类的run()方法,该run()方法的方法体就代表了线程需要完成的任务,因此把
run()方法称为线程执行体。 - 创建Thread子类的实例,即创建了线程对象
- 调用线程对象的start()方法来启动该线程
代码演示:
public class Demo01 {
public static void main(String[] args) {
//创建自定义线程对象
MyThread mt = new MyThread("新的线程!");
//开启新线程
mt.start();
//在主方法中执行for循环
for (int i = 0; i < 200; i++) {
System.out.println("main线程!"+i);
}
}
}
1.5 创建线程的方式二_实现方式
采用 java.lang.Runnable 也是非常常见的一种,我们只需要重写run方法即可。
- 定义Runnable接口的实现类,并重写该接口的run()方法,该run()方法的方法体同样是该线程的线程执行体。
- 创建Runnable实现类的实例,并以此实例作为Thread的target来创建Thread对象,该Thread对象才是真正
的线程对象。 - 调用线程对象的start()方法来启动线程。
public class MyRunnable implements Runnable{
@Override public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
public class Demo {
public static void main(String[] args) {
//创建自定义类对象 线程任务对象
MyRunnable mr = new MyRunnable();
//创建线程对象
Thread t = new Thread(mr, "小强");
t.start();
for (int i = 0; i < 20; i++) {
System.out.println("旺财 " + i);
}
}
}
1.6 匿名内部类方式
/**
* 请使用继承Thread类的方式定义一个线程类,在run()方法中循环10次,
* 每1秒循环1次,每次循环按“yyyy-MM-dd HH:mm:ss”的格式打印当前系统时间。
* 请定义测试类,并定义main()方法,启动此线程,观察控制台打印。
*/
public class Demo03 {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Date date = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String s = sdf.format(date);
System.out.println(s);
}
}
});
thread.start();
}
}
2 高并发及线程安全
- 高并发:是指在某个时间点上,有大量的用户(线程)同时访问同一资源。例如:天猫的双11购物节、12306的在线购票在某个时间点上,都会面临大量用户同时抢购同一件商品/车票的情况。
- 线程安全:在某个时间点上,当大量用户(线程)访问同一资源时,由于多线程运行机制的原因,可能会导致被访问的资源出现"数据污染"的问题。
2、1 多线程的安全性问题-可见性
Thread线程:
public class Thread extends java.lang.Thread {
public static int a = 0;
@Override
public void run() {
System.out.println("Thread0线程开始运行");
System.out.println("将a的值变成1");
a = 1;
System.out.println("改变成功!!");
}
}
测试类:
public class Test {
public static void main(String[] args) {
//创建子类对象
Thread thread = new Thread();
//调用start方法,运行run方法
thread.start();
while (true){
/**
* 可见性问题:由于while循环的速度太快,一直获取的是主内存中(也就是自己的内存空间)的a=0;
* 所以,死循环就一直执行下去了,
* 解决这个问题有两种方案:
* 方案1:在while循环里面让while循环睡两秒,降低循环效率
* 方案2:在Thread中添加变量修饰符;volatile
*/
if(Thread.a == 1){
System.out.println("main线程,判断a的值是否为1,结束循环");
break;
}
}
}
}
2、2 多线程的安全性问题-有序性
- 有些时候“编译器”在编译代码时,会对代码进行“重排”,例如:
int a = 10; //1
int b = 20; //2
int c = a + b; //3
第一行和第二行可能会被“重排”:可能先编译第二行,再编译第一行,总之在执行第三行之前,会将1,2编译完毕。1和2先编译谁,不影响第三行的结果。
但在“多线程”情况下,代码重排,可能会对另一个线程访问的结果产生影响:
2、2 多线程的安全性问题-原子性
Thread线程:
public class MyThread extends Thread {
public static int a = 0;
@Override
public void run() {
System.out.println("线程开始,打印a的值");
for (int i = 0; i < 10000; i++) {
a++;
}
System.out.println("改变完成");
}
}
测试类:
public class Test {
public static void main(String[] args) {
//创建对象
MyThread thread = new MyThread();
thread.start();
System.out.println("主线程开始执行:");
System.out.println("开始改变a的值");
for (int i = 0; i < 10000; i++) {
MyThread.a++;
}
/**
* 最终结论:
* 得到a的值可能不是20000,会少很多
* 每个线程访问money变量,都需要三步:
* 1).取money的值;
* 2).将money++
* 3).将money写回
* 这三步就不具有“原子性”——执行某一步时,很可能会被暂停,执行另外一个线程,就会导致变量的最终结果错误!!!!
*/
//先睡1秒,先让Thread线程执行完
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程改变a完成");
System.out.println(MyThread.a);
}
}
3 volatile关键字
- volatile是一个"变量修饰符",它只能修饰"成员变量",它能强制线程每次从主内存获取值,并能保证此变量不会被编译器优化。
- volatile能解决变量的可见性、有序性;
- volatile不能解决变量的原子性
3.1 volatile解决可见性
public class Thread extends java.lang.Thread {
public static volatile int a = 0;
@Override
public void run() {
System.out.println("Thread0线程开始运行");
System.out.println("将a的值变成1");
a = 1;
System.out.println("改变成功!!");
}
}
public class Test {
public static void main(String[] args) {
//创建子类对象
Thread thread = new Thread();
//调用start方法,运行run方法
thread.start();
while (true){
if(Thread.a == 1){
System.out.println("main线程,判断a的值是否为1,结束循环");
break;
}
}
}
}
注意:volatile关键字只能解决"变量"的可见性、有序性问题,并不能解决原子性问题