多线程编程
1、相关知识点介绍
-
进程:通常指某一个应用程序启动后加载一个任务
-
线程:一个进程中包含多个线程,在某一时刻执行某一个任务时,会调用某一个线程执行
-
默认的状态下,每一个线程之间不冲突(没有关联),不阻塞
-
new对象(隔离状态)
-
所有语言都拥有线程(唯一区别在于单线程、多线程)
-
线程中的误区
- cup和线程的执行速度有直接的关系,所有同样一段代码在不同的电脑跑出的速度是不一样的
- 即使是同一台电脑,跑相同的任务,执行时间也有可能不一样
-
任务调度:
- 老式的cpu通常一核心拥有一个线程(因为CPU个数是固定,所有同时存在的线程数也是固定的)
- windows上,同一时刻,只会使用CPU固定的线程数
- windows任务调度,每一时刻,只会分配一个进程使用
- 一个应用程序如果同时占用所有的线程(木马行为),windows会宕(dang)机
-
任务调度中,会给予每一个应用程序一定的时间执行任务,间隔一会收回线程,在给予其他应用程序使用
-
每一时刻执行任务的快慢由CPU的GHZ决定
-
CPU两个关键概念:
- CPU核心越多,那么线程越多,而线程越多,同时可执行的任务越多
- CPU 的GHZ(多少个G的内存)越高,执行每一个任务越快
-
windows默认会把线程优先级给到最前面的应用(可能会多给一些执行时间)
-
人的大脑反应时间是50—100毫秒左右(UE) 高于100毫秒后,大部分用户都会觉得卡顿
-
多线程:多个子线程同时在执行,即是多线程。因为多个子线程执行,相互之间是隔离状态,并且还随windows的任务调度而改变
-
同步化处理:让多个子线程形成队列,依次执行。同步化可以保证线程安全,但是会使性能和效率降低(因为队列需要排队处理)
-
死锁:当两个线程循环依赖于一对同步对象时将发生死锁状态。比如:进程A占有资源R1,等待进程B占有的资源Rr;进程B占有资源Rr,等待进程A占有的资源R1。而且资源R1和Rr只允许一个进程占用,即:不允许两个进程同时占用。结果,两个进程都不能继续执行,若不采取其它措施,这种循环等待状况会无限期持续下去,就发生了进程死锁。
-
线程之间的通信:synchronized 关键字阻止并发更新一个对象,但它没有实现线程通信。
2、线程概述
- 多任务处理有两种类型:
- 基于进程
- 基于线程
- 进程是指一种“自包容”的运行程序,有自己的地址空间;线程是进程内部单一的一个顺序控制流
- 基于进程的特点是允许计算机同时运行二个或更多的程序
- 基于线程的多任务处理环境中,线程是最小的处理单位
- 基于线程所需的开销更少
- 进程间调用涉及的开销比线程间通信多
可以将在一个Java虚拟机中运行的多线程程序看成是一个操作系统的进程
3、线程的创建
(1)继承Thread类
创建一个子线程的步骤:
- 继承Thread类:extends Thread
- 重写run方法
/**
* @author: tpp
* @version: 1.0
* @description: main.java
* @date: 2021/3/31 18:09
*/
public class mainTest{
public static int a=10;
public static void main(String[] args) {
System.out.println("main=="+Thread.currentThread());
MyThread m=new MyThread(); //误区:new对象永远不会创建一个新的线程
mainTest d=new mainTest();
//启动线程,不是调用run(误区),而是调用start方法
m.start(); //新的副本,不会影响后续的执行内容,子线程是非阻塞行为,启动子线程时,java虚拟机会向windows调度中心申请新的任务服务
d.check(); //不是子线程,check方法隶属于main方法的
System.out.println(mainTest.a);
}
public void check() {
mainTest.a=30;
System.out.println("check方法");
}
}
class MyThread extends Thread{//实现一个线程 核心:重写run方法
@Override
public void run() {
mainTest.a=20;
System.out.println("run=="+Thread.currentThread()); //检查当前方法被那一个线程调用
System.out.println("我的子线程");
}
}
多线程的调度(随机)
public class mainTest{
public static int a=10;
public static void main(String[] args) { // mian方法不仅是线程,还是主线程
//任何的线程都可以启动子线程
MyThread01 m=new MyThread01();
MyThread02 m1=new MyThread02();
MyThread03 m2=new MyThread03();
m.start(); //启动线程的这行代码,main方法不会管理 非阻塞
m1.start(); //启动线程的这行代码,main方法不会管理 非阻塞
m2.start(); //启动线程的这行代码,main方法不会管理 非阻塞
System.out.println(a);
}
}
class MyThread01 extends Thread{//实现一个线程 核心:重写run方法
@Override
public void run() {
mainTest.a=21;
System.out.println("run01=="+Thread.currentThread()); //检查当前方法被那一个线程调用
System.out.println("我的子线程");
}
}
class MyThread02 extends Thread{//实现一个线程 核心:重写run方法
@Override
public void run() {
mainTest.a=22;
System.out.println("run02=="+Thread.currentThread()); //检查当前方法被那一个线程调用
System.out.println("我的子线程");
}
}
class MyThread03 extends Thread{
@Override
public void run() {
mainTest.a=23;
System.out.println("run03=="+Thread.currentThread());
System.out.println("我的子线程");
}
}
sleep(时间毫秒数) 方法
(2)实现Runnable接口
因为Java是类单一继承,所以实际开发中,尽可能实现接口,而不要去继承类
创建一个子线程的步骤:
- 实现Runnable接口:implements Runnable
- 重写run方法
- 先创建一个实现接口类的对象,在创建一个Thread类的对象,最后把实现了接口的类的对象放入Thread类的构造函数中。即覆盖了Thread类中的run方法。
public class mainTest{
public static void main(String[] args) {
MyThread071 m=new MyThread071();//第三步
Thread t =new Thread(m);
t.start();
}
}
class MyThread071 implements Runnable{ //第一步
@Override
public void run() { //第二步
System.out.println(Thread.currentThread());
}
}
3、银行存取问题
/**
* @author: tpp
* @version: 1.0
* @description: ThreadTest3.java
* @date: 2021/4/4 0:09
*/
public class ThreadTest3{
/**
* 银行存取场景
*/
public static int a=0;
public static void main(String[] args) {
MyThread071 m01=new MyThread071();
MyThread072 m02=new MyThread072();
Thread t01 =new Thread(m01);
Thread t02 = new Thread(m02);
t01.start();
t02.start();
}
}
class MyThread071 implements Runnable{
@Override
public void run() {
for (int i = 0; i <5; i++) {
ThreadTest3.a +=200;
System.out.println("存钱="+ThreadTest3.a);
}
}
}
class MyThread072 implements Runnable{
@Override
public void run() {
for (int i = 0; i <5; i++) {
ThreadTest3.a -=200;
System.out.println("取钱="+ThreadTest3.a);
}
}
}
//输出结果:
//取钱=-200
//存钱=0
//存钱=0
//存钱=200
//存钱=400
//存钱=600
//取钱=-200
//取钱=400
//取钱=200
//取钱=0
总结:
取钱=-200 和 存钱=0 导致这两个错误的原因是因为任务调度中心不是按照一定顺序来调用线程的。存钱和取钱这两个线程是不分先后顺序,乱序的。任务调度会保证整个循环结束,但是不一定是一次性执行。
4、线程安全问题
public class ThreadTest3{
public static StringBuilder sb=new StringBuilder();
public static void main(String[] args) {
MyThread071 m01=new MyThread071();
MyThread072 m02=new MyThread072();
Thread t01 =new Thread(m01);
Thread t02 = new Thread(m02);
t01.start();
t02.start();
}
}
class MyThread071 implements Runnable{
@Override
public void run() {
for (int i = 0; i <50; i++) {
ThreadTest3.sb.append("200");//增加“200”字符串
System.out.println("存前=="+ThreadTest3.sb);
}
}
}
class MyThread072 implements Runnable{
@Override
public void run() {
for (int i = 0; i <50; i++) {
ThreadTest3.sb=ThreadTest3.sb.delete(ThreadTest3.sb.length()-3, ThreadTest3.sb.length());//删除“200”字符串
System.out.println("取钱=="+ThreadTest3.sb);
}
}
}
//输出结果:
//Exception in thread "Thread-1" java.lang.StringIndexOutOfBoundsException: String index out //of range: -3
//at java.lang.AbstractStringBuilder.delete(AbstractStringBuilder.java:756)
//at java.lang.StringBuilder.delete(StringBuilder.java:244)
//at test.MyThread072.run(ThreadTest3.java:38)
//at java.lang.Thread.run(Thread.java:748)
总结:
在理想场景中调度中心同时通过了两个子线程执行,如果执行的两个子线程,调用了一个共有的变量,那么此变量必须满足线程安全,否则会抛出异常。但是八种基本类型不受影响,报的异常Exception in thread "Thread-1"是指多线程调用了同一个非线程安全的变量,同一时刻两个线程调用了同一个变量,他们之间形成了竞争关系,所以如果变量没有线程安全,就会报错。变量的线程安全性,是保证多个线程在调度时,不会出现同时执行。
5、synchronized(不能修饰类和属性,可以修饰方法)
为什么要用synchronized同步化?
答:当同一时刻多个线程调用了同一个资源(即同一个对象的方法或者变量),比如一个线程可能尝试从一个文件中读取数据,而另一个线程则尝试在同一个文件修改数据,在此情况下,数据可能会变得不一致,为了确保在任何时间点一个共享的资源只被一个线程使用,则要使用同步化关键词synchronized来进行标识。
public class ThreadTest3{
public static int sb=0;
public static void main(String[] args) {
Syncobj obj=new Syncobj();
MyThread071 m01=new MyThread071(obj,true);
MyThread071 m02=new MyThread071(obj,false);
Thread t01 =new Thread(m01);
Thread t02 = new Thread(m02);
t01.start();
t02.start();
//同步化处理 保证数据的正确性 保证非竞争
// 所有的线程必须调用同一个对象的指定的同步化方法(核心)
}
}
class Syncobj{
//如果在实际开发中,遇到了多个线程同时调用一个对象的同一方法,那么必须保证此方法是synchronized同步化的
public synchronized void check(boolean a) {
if(a) {
ThreadTest3.sb+=200;
System.out.println("存前=="+ThreadTest3.sb);
}else {
ThreadTest3.sb-=200;
System.out.println("取前=="+ThreadTest3.sb);
}
}
}
class MyThread071 implements Runnable{
Syncobj obj;
boolean a;
public MyThread071(Syncobj obj,boolean a) {
this.a = a;
this.obj = obj;
}
public MyThread071() {
}
@Override
public void run() {
for(int i=0;i<100;i++) {
obj.check(a);
}
}
}
public void check(){
synchronized(this){
//this代表 哪一个对象调用的这个方法,那么this就是这个对象
}
}
6、volatile(轻量级的synchronized )
用法:只能用来修饰变量,无法修饰方法及代码块。volatile的用法比较简单,只需要在声明一个可能被多线程同时访问的变量时,使用volatile修饰就可以了。
作用:保证了变量的可见性(visibility)。被volatile关键字修饰的变量,如果值发生了变更,其他线程立马可见,避免出现脏读的现象。
7、多线程与单线程性能比较
/**
* @author: tpp
* @version: 1.0
* @description: ThreadTest.java
* @date: 2021/4/3 21:14
*/
//用一个线程计算从1+……+5000000000
long sum = 0;
long startTime = System.currentTimeMillis();
for (long i = 0 ; i < 5000000001L ; i++)
{
sum += i;
}
System.out.println(sum);
System.out.println(System.currentTimeMillis() - startTime);
//-5946744071209551616 总和
//1436 用了将近1.4秒(不同电脑性能不同速度,性能越好,时间越短)
/**
* @author: tpp
* @version: 1.0
* @description: FiveThread.java
* @date: 2021/4/3 23:19
*/
//通过一个同步化方法完成5个线程的值合并
public class FiveThread {
public static void main(String[] args) {
/**
* 5 线程
* 1、1+10亿
* 2、10亿到20亿
* 3、20亿到30亿
* 4、30亿到40亿
* 5、40亿到50亿
*/
SumObj sumObj = new SumObj(System.currentTimeMillis());
Thread thread1 = new Thread(new MyThread01(sumObj));
Thread thread2 = new Thread(new MyThread02(sumObj));
Thread thread3 = new Thread(new MyThread03(sumObj));
Thread thread4 = new Thread(new MyThread04(sumObj));
Thread thread5 = new Thread(new MyThread05(sumObj));
thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread5.start();
}
}
class SumObj{
long sum=0;
long startTime=0;
public SumObj(long startTime) {
this.startTime = startTime;
}
public synchronized void check(long s){
sum+=s;
System.out.println(sum);
System.out.println(System.currentTimeMillis()-startTime);
}
}
class MyThread01 implements Runnable{
SumObj obj;
public MyThread01(SumObj obj) {
this.obj = obj;
}
@Override
public void run() {
long sum=0;
for (long i = 1; i <1000000001L ; i++) {
sum += i;
}
obj.check(sum);
}
}
class MyThread02 implements Runnable{
SumObj obj;
public MyThread02(SumObj obj) {
this.obj = obj;
}
@Override
public void run() {
long sum=0;
for (long i = 1000000001; i <2000000001L ; i++) {
sum += i;
}
obj.check(sum);
}
}
class MyThread03 implements Runnable{
SumObj obj;
public MyThread03(SumObj obj) {
this.obj = obj;
}
@Override
public void run() {
long sum=0;
for (long i = 2000000001L; i <3000000001L ; i++) {
sum += i;
}
obj.check(sum);
}
}
class MyThread04 implements Runnable{
SumObj obj;
public MyThread04(SumObj obj) {
this.obj = obj;
}
@Override
public void run() {
long sum=0;
for (long i = 3000000001L; i <4000000001L ; i++) {
sum += i;
}
obj.check(sum);
}
}
class MyThread05 implements Runnable{
SumObj obj;
public MyThread05(SumObj obj) {
this.obj = obj;
}
@Override
public void run() {
long sum=0;
for (long i = 4000000001L; i <5000000001L ; i++) {
sum += i;
}
obj.check(sum);
}
}
//2500000000500000000 从1+……+1000000000的和
//520 计算所需时间
//4000000001000000000 从1000000001+……+2000000000的和
//533 计算所需时间(与上面计算的时间之和)
//8500000001500000000 从2000000001+……+3000000000的和
//538 计算所需时间(与上面计算的时间之和)
//9000000002000000000 从3000000001+……+4000000000的和
//540 计算所需时间(与上面计算的时间之和)
//-5946744071209551616 从4000000001+……+5000000000的和
//543 计算所需时间(与上面计算的时间之和) 这是总共所花的时间
总结:
1、多线程任务在正常情况下,肯定比单线程执行速度快
2、随着线程数的增加,提升的速度会相应降低
3、线程的多少一般由cpu决定
8、一个类实现多线程的处理
/**
* @author: tpp
* @version: 1.0
* @description: ThreadTest.java
* @date: 2021/4/3 22:14
*/
//通过一个同步化方法完成5个线程的值合并,消除冗余代码,将线程类合为一个
public static void main(String[] args) {
/**
* 5 线程
* 1、1+10亿
* 2、10亿到20亿
* 3、20亿到30亿
* 4、30亿到40亿
* 5、40亿到50亿
*
*/
SumObj sumObj = new SumObj(System.currentTimeMillis());
Thread thread1 = new Thread(new MyThread01(sumObj,1L));
Thread thread2 = new Thread(new MyThread01(sumObj,1000000001L));
Thread thread3 = new Thread(new MyThread01(sumObj,2000000001L));
Thread thread4 = new Thread(new MyThread01(sumObj,3000000001L));
Thread thread5 = new Thread(new MyThread01(sumObj,4000000001L));
thread1.start();
thread2.start();
thread3.start();
thread4.start();
thread5.start();
}
}
class SumObj{
long sum=0;
long startTime=0;
public SumObj(long startTime) {
this.startTime = startTime;
}
public synchronized void check(long s){
sum+=s;
System.out.println(sum);
System.out.println(System.currentTimeMillis()-startTime);
}
}
class MyThread01 implements Runnable{
SumObj obj;
long aLong;
public MyThread01(SumObj obj,long aLong) {
this.obj = obj;
this.aLong=aLong;
}
@Override
public void run() {
long sum=0;
for (long i = aLong; i <aLong+1000000001L ; i++) {
sum += i;
}
obj.check(sum);
}
}
//4500000005500000001 从1+……+1000000000的和
//898 计算所需时间
//7000000009000000002 从1000000001+……+2000000000的和
//905 计算所需时间(与上面计算的时间之和)
//7500000010500000003 从1000000001+……+2000000000的和
//928 计算所需时间(与上面计算的时间之和)
//-7446744058709551612 从1000000001+……+2000000000的和
//944 计算所需时间(与上面计算的时间之和)
//-5946744056209551616 从1000000001+……+2000000000的和
//958 计算所需时间(与上面计算的时间之和) 这是总共所花的时间
9、生产者与消费者模型
wait-notify机制:
- 为避免轮流检测,Java提供了一个精心设计的线程间通信机制,使用wait()、notify()和notifyAll()方法。
- wait()方法:告知被调用的线程退出监视器并进入等待状态,直到其他线程进入相同的监视器并调用notify方法
- notify()方法:通知同一个对象上第一个调用wait()线程。
- notifyAll()方法:通知调用wait()的所有线程,具有最高优先级的线程将先运行。
- 这些方法都是Object类中的final方法实现的
- 这三个方法仅在synchronized方法中才能被调用
/**
* @author: tpp
* @version: 1.0
* @description: ThreadTest2.java
* @date: 2021/4/4 1:07
*/
public class ThreadTest2 {
public static void main(String[] args) {
ThreadTest2 test2 = new ThreadTest2();
test2.check();
}
public void check(){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//运行结果:
//Exception in thread "main" java.lang.IllegalMonitorStateException
//wait()、notify()、notifyAll()这三个状态会出现上面的异常错误
//原因:这三个方法仅在synchronized方法中才能被调用
//改:
public synchronized void check(){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
生产者与消费者代码实现:
/*
* @author: tpp
* @version: 1.0
* @description: proConsumer.java
* @date: 2021/4/4 12:45
*/
public class proConsumer {
/*
* 生产者(3个) 消费者(2个)
* 仓库max(15) min(0)
*/
public static void main(String[] args) {
warehouse w = new warehouse();
new Thread(new producer(w,1100),"生产者01").start();
new Thread(new producer(w,1200),"生产者02").start();
new Thread(new producer(w,1300),"生产者03").start();
new Thread(new consumer(w,9000),"消费者01").start();
new Thread(new consumer(w,1150),"消费者02").start();
}
}
class producer implements Runnable{
private warehouse w;
private long randomTime;
public producer(warehouse w,long randomTime){
this.w = w;
this.randomTime = randomTime;
}
@Override
public void run() {
while(true){
try {
Thread.sleep(randomTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
w.produce();
}
}
}
class consumer implements Runnable{
private warehouse w;
private long randomTime;
public consumer(warehouse w, long randomTime){
this.w = w;
this.randomTime = randomTime;
}
@Override
public void run() {
while(true){
try {
Thread.sleep(randomTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
w.consume();
}
}
}
class warehouse{
private int a = 0;
public synchronized void produce(){ //同步化的作用是保证每次多线程执行都是有效的
this.notifyAll(); //会把曾经调用过此方法wait()的线程全部唤醒
while(a == 15){ //while的作用:一直等到此线程被wait掉,才会结束
try { //如果用if只能管控一次,一次过后就会执行后续代码
System.out.println(Thread.currentThread()+"满仓等待");
this.wait(); //如果wait()成功,相当于return整个方法(终止方法),就不会执行此方法中的后续代码
//在同步化方法中,哪一个线程被wait()时,那么内存会记住哪个线程在哪一个方法中被wait()
//wait()后面不能跟代码
} catch (InterruptedException e) {
e.printStackTrace();
}
}
a++;
System.out.println(Thread.currentThread()+" 制造 + 1 a = "+a);
}
public synchronized void consume(){
this.notifyAll(); //唤醒不了produce()方法的线程,只能唤醒consume()方法的线程
while(a == 0){
try {
System.out.println(Thread.currentThread()+"空仓走人");
this.wait();
//wait()后面不能跟代码
} catch (InterruptedException e) {
e.printStackTrace();
}
}
a--;
System.out.println(Thread.currentThread()+" 消费 - 1 a = "+a);
}
}
//输出结果:
//Thread[生产者01,5,main] 制造 + 1 a = 1
//Thread[消费者02,5,main] 消费 - 1 a = 0
//Thread[生产者02,5,main] 制造 + 1 a = 1
//Thread[生产者03,5,main] 制造 + 1 a = 2
//……
//Thread[生产者03,5,main] 制造 + 1 a = 14
//Thread[消费者02,5,main] 消费 - 1 a = 13
//Thread[生产者02,5,main] 制造 + 1 a = 14
//Thread[生产者01,5,main] 制造 + 1 a = 15
//Thread[消费者02,5,main] 消费 - 1 a = 14
//Thread[生产者03,5,main] 制造 + 1 a = 15
//Thread[生产者02,5,main]满仓等待
//Thread[生产者01,5,main]满仓等待
//Thread[生产者02,5,main]满仓等待
//分析:因为生产者有3个,消费者只有两个,最终的结果就是生产者一直加直到满仓等待
多线程面试题
1.什么是线程?
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。程序员可以通过它进行多处理器编程,通过多线程处理,可以一定量的提升程序执行速度。
2.线程和进程有什么区别?
一个进程是一个独立(self contained)的运行环境,它可以被看作一个程序或者一个应用。而线程是在进程中执行的一个任务。线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。别把它和栈内存搞混,每个线程都拥有单独的栈内存用来存储本地数据。
3.如何在Java中实现线程?
有两种创建线程的方法:一是实现Runnable接口,然后将它传递给Thread的构造函数,创建一个Thread对象;二是直接继承Thread类。
4.用Runnable还是Thread?
优先实现接口,因为Java中类是单一继承,但是接口可以多重实现。
5.Thread 类中的start() 和 run() 方法有什么区别?
start()方法被用来启动新创建的线程,使该被创建的线程状态变为可运行状态。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。如果我们调用了Thread的run()方法,它的行为就会和普通的方法一样,直接运行run()方法。为了在新的线程中执行我们的代码,必须使用Thread.start()方法。
6.synchronized关键字有什么作用?
synchronized可以锁住指定方法或者对象,目的是了多个线程在同时调用此方法或者对象时,可以进行排队处理(类似于悲观锁机制)从而保证多线程运行时的安全。
7、生产者与消费者
wait-notify机制
wait() :通知被调用的线程退出监视器进入等待状态,直到其他线程进入相同的监视器并调用notify方法 wait后面不能接代码块
notify() :随机唤醒一个线程的方法
notifyAll() :唤醒所有的线程的方法
这些方法是作为Object类中的final类实现的
这些方法必须在synchronized方法中使用
8、怎么样控制两个线程交替打印,例如:+1(线程1)+2(线程2)+3(线程3)……
/**
* @author: tpp
* @version: 1.0
* @description: ThreadTest4.java
* @date: 2021/4/4 17:48
*/
public class ThreadTest4 {
public static void main(String[] args) {
print print = new print();
Thread t1 = new Thread(new printTest(print));
Thread t2 = new Thread(new printTest(print));
t1.start();
t2.start();
}
}
class printTest implements Runnable{
print p;
public printTest(print p){
this.p = p;
}
@Override
public void run() {
while(true){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
p.prints();
}
}
}
class print{
private int a = 0;
public synchronized void prints(){
a++;
System.out.print("+"+a+"(进程"+a+")");
}
}