Java多线程——面试重点
多线程
/*
主线程:执行主方法(main)的线程,叫主线程
程序执行的入口就是main方法,JVM从main方法开始
由上到下依次执行程序
*/
public class Demo01MainThread {
public static void main(String[] args) {
Person p1 = new Person("张三");
p1.run();
System.out.println(0/0);
Person p2 = new Person("李四");
p2.run();
}
}
public class Person {
private String name;
public Person(String name) {
this.name = name;
}
public void run(){
for (int i = 0; i < 20; i++) {
System.out.println(name+"-->"+i);
}
}
}
5.线程的调度
分时调度:所有的线程轮流使用cpu的使用权,平均每个线程占用cpu的时间
抢占式调度:优先让优先级较高的线程使用cpu,如果线程的优先级相同,会随机选择一个执行(线程的随机性)。Java使用的就是抢占式调度
6.多线程程序创建的第一种方式:继承Thread类
/*
多线程程序创建的第一种方式:继承Thread类
java.lang.Thread:是一个描述线程的类
线程 是程序中的执行线程。Java 虚拟机允许应用程序并发地运行多个执行线程。
每个线程都有一个优先级,高优先级线程的执行优先于低优先级线程。
实现步骤:
1.创建一个类继承Thread类
2.在Thread的子类中,重写Thread类中的run方法,设置线程任务(开启的新的线程目的是什么:查杀病毒,清理垃圾)
3.创建Thread类的子类对象
4.调用继承自Thread类中的start方法,开启新的线程执行run方法
void start() 使该线程开始执行;Java 虚拟机调用该线程的 run 方法。
调用start方法的结果是两个线程并发地运行;当前线程(main线程:运行main方法中的代码)和另一个线程(开启的新线程:执行run方法中的代码)。
多次启动一个线程是非法的(一个线程对象只能调用一次start方法)。特别是当线程已经结束执行后,不能再重新启动。
注意:
java属于抢占式调度,优先让优先级高的线程使用cpu,如果线程的优先级是相同的,那么随机选择一个执行
*/
public class Demo01MyThread {
public static void main(String[] args) {
//3.创建Thread类的子类对象
MyThread mt = new MyThread();
//4.调用继承自Thread类中的start方法,开启新的线程执行run方法
mt.start();
//main线程开启新的线程之后,会继续执行main方法中的代码
for (int i = 0; i < 20; i++) {
System.out.println("main-->"+i);
}
}
}
//1.创建一个类继承Thread类
public class MyThread extends Thread {
//2.在Thread的子类中,重写Thread类中的run方法,设置线程任务
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("run-->"+i);
}
}
}
7.多线程内存图解
8.获取多线程名称
/*
线程的名称:
主线程: main
新的线程: Thread-0,Thread-1,...,Thread-n
*/
public class Demo01MyThread {
public static void main(String[] args) {
//System.out.println(0/0);
MyThread mt = new MyThread();
mt.start();
new MyThread().start();
new MyThread().start();
//执行main方法的线程就是main线程
System.out.println(Thread.currentThread().getName());
}
}
/*
获取线程的名称:
1.可以使用Thread类中的方法getName
String getName() 返回该线程的名称。
注意:
只能在Thread类的子类中使用
2.我们可以获取当前正在执行的线程Thread,再使用Thread类中的方法getName获取线程名称
static Thread currentThread() 返回对当前正在执行的线程对象的引用。
注意:
可以在任意的位置使用 Thread.currentThread();
*/
public class MyThread extends Thread{
@Override
public void run() {
//System.out.println(0/0);
//1.可以使用Thread类中的方法getName
//String name = getName();//MyThread类继承了Thread类,所以可以直接使用继承自Thread类中的getName方法
//System.out.println(name);
//2.我们可以获取当前正在执行的线程Thread
//Thread currentThread = Thread.currentThread();
//再使用Thread类中的方法getName获取线程名称
//String name = currentThread.getName();
//System.out.println(name);
System.out.println(Thread.currentThread().getName());
}
}
9 Thread类中方法sleep
/*
Thread类中的方法:
static void sleep(long millis) :参数传递毫秒值
在指定的毫秒数内让当前正在执行的线程休眠(暂停执行), 让线程睡眠,睡醒了继续执行
*/
public class Demo01Sleep {
public static void main(String[] args) {
System.out.println("程序10秒钟之后执行");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}
}
public class Demo02Sleep {
public static void main(String[] args) throws InterruptedException {
//秒表:一秒钟执行1次
for (int i = 1; i <= 60; i++) {
System.out.println(i);
//程序睡眠1秒钟,在继续执行
Thread.sleep(1000);
}
}
}
10.多线程程序创建的第二种方式:实现Runnable接口
/*
多线程程序创建的第二种方式:实现Runnable接口重点)
java.lang.Runnable接口:
Runnable 接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为 run 的无参数方法。
java.lang.Thread类 implements Runnable接口
Thread类的构造方法:
Thread(Runnable target)分配新的 Thread 对象。
Thread(Runnable target, String name) 分配新的 Thread 对象。
实现步骤:
1.创建一个实现类实现Runnable接口
2.在实现类中重写Runnable接口中的run方法,设置线程任务
3.创建Runnable接口的实现类对象
4.创建Thread类对象,构造方法中传递Runnable接口的实现类对象
5.调用Thread类中的start方法,开启新的线程,运行run方法
*/
public class Demo01Runnable {
public static void main(String[] args) {
//3.创建Runnable接口的实现类对象
RunnableImpl run = new RunnableImpl();
//4.创建Thread类对象,构造方法中传递Runnable接口的实现类对象
Thread t = new Thread(run);
//5.调用Thread类中的start方法,开启新的线程,运行run方法
t.start();
//主线程在开启先的线程之后,会执行执行main方法中代码
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
//1.创建一个实现类实现Runnable接口
public class RunnableImpl implements Runnable {
//2.在实现类中重写Runnable接口中的run方法,设置线程任务
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println(Thread.currentThread().getName()+"-->"+i);
}
}
}
11.创建多线程程序两种方式的区别
/*
创建多线程程序两种方式的区别
1.使用实现Runnable接口的方式实现多线程程序,可以避免单继承的局限性
a.类继承了Thread类,就不能继承其他的类
b.类实现了Runnable接口,还可以继承其他的类
2.使用实现Runnable接口的方式实现多线程程序,把设置线程任务和开启线程进行了解耦(解除了耦合性增强了扩展性)
a.类继承了Thread类,在run方法中设置的什么线程任务,创建对象就执行什么线程任务
b.类实现了Runnable接口的目的:重写run方法设置线程任务;创建Thread类对象的目的:传递线程任务,执行线程任务
*/
public class Demo01ThreadAndRunnable {
public static void main(String[] args) {
MyThread mt = new MyThread();
mt.start();//只能执行一个任务
//在Thread类的构造方法中,可以传递不同的任务,执行不同的任务
//new Thread(new RunnableImpl1()).start();//执行打印20次你好的任务
new Thread(new RunnableImpl2()).start();//执行打印20次Hello的任务
}
}
public class MyThread extends Thread{
@Override
public void run() {
System.out.println("继承Thread类的方式,实现多线程程序!");
}
}
public class RunnableImpl2 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 20; i++) {
System.out.println("Hello");
}
}
}
12.匿名内部类的方式实现多线程程序
/*
匿名内部类的方式实现多线程程序(重点)
匿名内部类:
把子类继承父类,重写父类的方法,创建子类对象合成一步完成
把实现类实现接口,重写接口中的方法,创建实现类对象合成一步完成
作用:简化代码,不用创建子类和实现类
格式:
new 父类|接口(){
重写父类|接口中的方法
};
注意:
匿名内部类最终产生的是一个子类对象|实现类对象
*/
public class Demo01Thread {
public static void main(String[] args) {
//父类: Thread
//new MyThread().start();
new Thread(){
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}.start();
//接口: Runnable
//多态 Runnable r = new RunnableImp(); new Thread(r).start();
Runnable r = new Runnable(){
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
};
new Thread(r).start();
//new Thread(new RunnableImp()).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
}
}).start();
}
}
高并发及线程安全
1.高并发及线程安全概念
1.1 高并发: 在某个时间点上,有多个线程同时访问某个资源。例如:双十一,过年时候的12306。
1.2线程安全性问题: 当多个线程同时访问一个资源(例如:同一变量、同一数据库、同一文件……),而且访问同一资源的代码不具有“原子性”,这时对这一资源的访问就会产生安全性问题——导致此资源最终的结果是错误。
1.3高并发所产生的安全性问题主要表现:
1).可见性: 指当多个线程访问同一变量时,一个线程改变了这个变量的值,其他线程可以立刻看到修改后的值。
2).有序性: 即程序执行的顺序按照代码的先后顺序执行
3).原子性: 即一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
2.高并发问题——可见性
产生原因:定义一个变量。一个线程修改,另一个线程由于访问频率太快,导致一直使用本线程区内的变量副本,而没有实时的到主内存中去获取变量的新值。
//创建一个类继承Thread类
public class MyThread extends Thread{
//定义一个共享的静态成员变量,供多个线程一起使用
public static int a = 0;
//重写Thread类中的run方法,设置线程任务
@Override
public void run() {
System.out.println("Thread-0线程开始执行了,先睡眠2秒钟");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread-0线程睡醒了,改变a的值为1");
a=1;
System.out.println("Thread-0线程的线程任务执行完毕!");
}
}
public class Demo01Visible {
public static void main(String[] args) {
//创建Thread类的子类对象
MyThread mt = new MyThread();
//调用继续自Thread类中的start方法,开启新的线程执行run方法
mt.start();
System.out.println("main线程,会继续执行main方法中的代码");
while (true){
if(MyThread.a==1){
System.out.println("main线程,判断变量a的值为1,结束死循环!");
break;
}/*else{
System.out.println("死循环");
}*/
}
}
}
3.Java内存模型JMM
概述:JMM(Java Memory Model)Java内存模型,是java虚拟机规范中所定义的一种内存模型。
Java内存模型(Java Memory Model)描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量存储到内存和从内存中读取变量这样的底层细节。
所有的共享变量都存储于主内存。这里所说的变量指的是实例变量(成员变量)和类变量(静态成员变量)。不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。每一个线程还有着自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量,不同线程之间也不能直接访问
对工作内存中的变量,线程间变量的值的传递需要通过主内存完成。
可见性的原理
4.高并发问题——有序性
1).有序性:多行代码的编写顺序和编译顺序。
有些时候,编译器在编译代码时,为了提高效率,会对代码“重排”:
.java文件
int a = 10; //第一行
int b = 20; //第二行
int c = a * b; //第三行
在执行第三行之前,由于第一行和第二行的先后顺序无所谓,所以编译器可能会对“第一行”和“第二行”进行代码重排:
.class文件
int b = 20;
int a = 10;
int c = a * b;
2).但在多线程环境下,这种重排可能是我们不希望发生的,因为:重排,可能会影响另一个线程的结果,所以我们不需要代码进行重排
5.高并发问题——原子性
public class MyThread extends Thread{
public static int money = 0;
@Override
public void run() {
System.out.println("Thread-0线程开始改变money的值!");
for (int i = 0; i < 10000; i++) {
money++;
}
System.out.println("Thread-0线程执行完毕!");
}
}
public class Demo01Thread {
public static void main(String[] args) throws InterruptedException {
//开启新的线程
MyThread mt = new MyThread();
mt.start();
System.out.println("main继续往下执行");
System.out.println("main线程开始改变money的值!");
for (int i = 0; i < 10000; i++) {
MyThread.money++;
}
System.out.println("main线程暂停1秒,让Thread-0执行完毕!");
Thread.sleep(1000);
System.out.println("最终money为:"+MyThread.money);
}
}
main线程会继续执行下边的代码!
main线程开始改变变量money的值!
main线程暂停1秒钟,等待Thread-0线程执行完毕,在继续执行!
Thread-0线程开始改变money的值!
Thread-0线程线程任务执行完毕!
最终money的值为:20000
main线程会继续执行下边的代码!
main线程开始改变变量money的值!
Thread-0线程开始改变money的值!
Thread-0线程线程任务执行完毕!
main线程暂停1秒钟,等待Thread-0线程执行完毕,在继续执行!
最终money的值为:12484
main线程会继续执行下边的代码!
main线程开始改变变量money的值!
Thread-0线程开始改变money的值!
main线程暂停1秒钟,等待Thread-0线程执行完毕,在继续执行!
Thread-0线程线程任务执行完毕!
最终money的值为:17965
每个线程访问money变量,都需要三步:
1).取money的值;
2).将money++
3).将money写回
这三步就不具有原子性——执行某一步时,很可能会被暂停,执行另外一个线程,就会导致变量的最终结果错误!!!!
解决办法见下篇
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。