下面学习线程的基本知识,包括线程与进程区别、开辟新线程的方法、线程安全隐患、死锁、等待唤醒机制和生产者消费者模式等知识。
线程
线程:负责进程中程序的执行,是进程的一个执行单元,一个进程中允许有多个线程,为多线程,如果只有一条线程,称之为单线程。一个进程至少有一条线程。
多线程并没有真的提高了运行速度,在同一时刻只有一条线程在执行,由于切换速度很快,感觉好像很多线程在同时执行。
进程:正在内存中运行的应用程序就是进程。
多线程的优点:
(1)提高用户体验,参考网页打开时图片和文字加载过程
(2)提高了CPU的利用率,线程执行需要和计算机硬件进行交互,当线程和计算机硬件进行交互时,此时CPU是空置的,空置的CPU可以执行其他的线程,因此多线程可以提高CPU利用率。
抢占式调度:Java使用抢占式调度,线程是有优先级的,理论上先执行优先级高的线程,如果优先级相同,会随机执行一条线程。对于CPU的一个核来说,同时只有一条线程在执行,但是线程在CPU核上进行高速的切换,所以感觉是同时执行。
JVM启动时会给一条主线程,给main方法去执行代码,还会给一条线程给gc用于垃圾回收。
在Java中开辟新的线程
(1)写一个子类继承自线程顶级父类Thread,重写run方法,将需要被新线程执行的方法写到里面,然后使用子类对象的start方法开启新的线程。
package com.boe.thread;
public class ThreadDemo {
public static void main(String[] args) {
//调用子类的线程
MyThread t=new MyThread();
//只是调用子类的run方法,但是没有开辟线程
//t.run();
//需要开辟新的线程,使用start方法
t.start();
//调用主方法的线程
for(int i=0;i<5;i++){
System.out.println("主类的线程i:"+i);
}
}
}
class MyThread extends Thread{
//在run方法中写需要开辟新的线程执行的代码
@Override
public void run(){
for(int i=0;i<5;i++){
System.out.println("子类的线程i:"+i);
}
}
}
测试结果
经典面试题:run方法和start方法的区别,run方法并不会开启新的线程,只是在原来线程中去执行run方法里的代码,start方法会开启新的线程,并执行run方法里的代码。
(2)写一个类实现Runnable接口,并重写里面run方法,创建类对象将其传入Thread构造方法中创建一个Thread线程对象,然后调用Thread对象的start方法启动线程,这个方法最为常用。
package com.boe.thread;
public class RunnableDemo {
public static void main(String[] args) {
//方式1
//创建Runnable接口的实现类对象
MyRunnableThread myThread=new MyRunnableThread();
//再创建Thread对象,并将实现类通过构造方法传入
Thread t=new Thread(myThread);
//调用Thread对象的start方法,开启新的线程
t.start();
//方式2 也可以采用匿名内部类来开启一个线程
new Thread(new Runnable(){
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println("匿名内部类的线程的i:"+i);
}
}
}).start();
for(int i=0;i<10;i++){
System.out.println("主线程的线程的i:"+i);
}
}
}
//写一个类实现Runnable接口
class MyRunnableThread implements Runnable{
@Override
public void run() {
for(int i=0;i<10;i++){
System.out.println("实现Runnable接口的线程的i:"+i);
}
}
}
测试结果
(3)实现Callable接口,后续添加
线程安全
线程安全隐患:多线程执行的结果和单线程执行的结果可能不一样,叫做线程安全隐患,参考卖票案例,会出现重复票,还有票号为0的,因为线程优先级一样的情况下,执行次序是无序的,导致结果异常,为了解决这个问题加同步锁解决。
package com.boe.thread;
/**
* 开启三个线程同时卖票,共有100张票
* @author clyang
*/
public class TicketDemo {
public static void main(String[] args) {
//创建卖票窗口
TicketWindow c=new TicketWindow();
//创建三个线程对象,三个线程共用一个锁资源
Thread t1=new Thread(c,"窗口1");
Thread t2=new Thread(c,"窗口2");
Thread t3=new Thread(c,"窗口3");
//同时卖票
t1.start();
t2.start();
t3.start();
}
}
//定义卖票窗口
class TicketWindow implements Runnable{
private static int count=100;//票数
//加一个锁资源
Object obj=new Object();
@Override
public void run() {
while(true){
//睡眠0.001秒,测试线程安全隐患
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
//使用shift+ctrl+/多行带星号注释
//1 加同步代码块
/*synchronized(obj){
if(count>0){
System.out.println(Thread.currentThread().getName()+"正在卖票"+count--);
}
}*/
//2 同步方法
//method();
//3 静态同步方法
this.staticMethod();
}
}
//同步方法,锁资源就是this
public synchronized void method(){
if(count>0){
System.out.println(Thread.currentThread().getName()+"正在卖票"+count--);
}
}
//静态同步方法,锁资源就是类名.class
public static synchronized void staticMethod(){
if(count>0){
System.out.println(Thread.currentThread().getName()+"正在卖票"+count--);
}
}
}
添加同步锁后执行不会有线程安全问题,可以实现正常卖票。
出现的线程安全的条件:有多个线程、多个线程共享资源、有写操作。
解决方案:
(1)同步代码块,一条线程中核心代码没有执行完,其他线程就不要执行。同步代码块可以保证同一时刻最多只有一条线程在执行代码块中的逻辑,其书写格式如下:
synchronized(锁资源){
同步代码块
}
锁资源可以是任意对象,但是需要保证多个线程共用同一个锁资源才可以。但是同步代码块不能随便加,否则会造成执行效率降低,最差会变成单线程,因此最好将能造成安全隐患的代码加入同步代码块,其他正常执行。
(2)同步方法,保证方法中最多只有一条线程在执行,锁资源是this,格式如下
public synchronized 返回值类型 方法名(参数列表){}
(3)静态同步方法,保证方法中最多只有一条线程在执行,锁资源是类名.class,格式如下
public synchronized 返回值类型 方法名(参数列表){}
经典面试题,简述HashMap、Hashtable和ConcurrentHashMap的区别
HashMap:是异步线程不安全的,但是效率高
Hashtable:是同步线程安全的,但是效率低
ConcurrentHashMap:是异步线程安全的,底层采用分桶锁的机制,效率比Hashtable高很多
经典面试题
同步和异步:同步是在程序中最多只有一条线程,异步是在程序中允许有多条线程。
同步一定线程安全 √
线程安全一定同步 ×
异步一定线程安全 ×
异步一定有线程安全隐患 ×
线程不安全一定异步 √
死锁
由于锁互相嵌套导致的互相锁死的现象,称之为死锁,参考打印机和扫描仪案例。
package com.boe.thread;
/**
* 死锁案例
* @author clyang
*/
public class DeadLockDemo {
static Printer p=new Printer();
static Scan s=new Scan();
public static void main(String[] args) {
//开一线程,先打印一秒,再扫描
new Thread(new Runnable(){
@Override
public void run() {
synchronized(p){
p.print();
//等待1秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//扫描
synchronized(s){
s.scan();
}
}
}
}).start();
//开一个线程,先扫描一秒,再打印
new Thread(new Runnable(){
@Override
public void run() {
synchronized(s){
s.scan();
//等待1秒
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//打印
synchronized(p){
p.print();
}
}
}
}).start();
}
}
//打印机
class Printer{
public void print(){
System.out.println("打印机正在打印......");
}
}
//扫描仪
class Scan{
public void scan(){
System.out.println("扫描仪正在扫描......");
}
}
测试结果发现,每个线程开启都分别执行了一次打印和扫描,但是后续不再执行,分析发现第一个线程执行完打印后,准备执行扫描但是发现扫描锁资源s被占了,因此无法继续执行扫描并释放打印锁资源p,而二个线程执行完扫描后,准备执行打印发现打印锁资源p被占了,因此无法继续执行打印并释放扫描锁资源s,从而导致两个线程一直等待对方释放锁资源,这就形成了死锁。
等待唤醒机制
使用一个对象加锁,那么必须使用相同的对象进行等待和唤醒,参考模拟两个学生轮流问问题和生产者-消费模型。
package com.boe.thread;
/**
* 等待唤醒机制 object的方法wait notify
* @author clyang
*
*/
public class AwakeDemo {
public static void main(String[] args) {
Student s=new Student();
s.setAge(44);s.setName("李四");
//开启一个线程切换学生
new Thread(new ChangeStudent(s)).start();
//开启一个线程问问题
new Thread(new Ask(s)).start();
}
}
//定义一个线程模拟学生问问题
class Ask implements Runnable{
private Student s;
public Ask(Student student){
this.s=student;
}
@Override
public void run() {
while(true){
synchronized(s){
if(!s.flag){
//此时在切换学生,先等待
try {
s.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("老师,我是"+s.getName()+",年龄"+s.getAge()+",我在问问题");
//标识位置为false,代表下次切换学生
s.flag=false;
//唤醒切换学生的线程,将其从线程池中取出来继续执行
s.notify();
}
}
}
}
//定义一个线程模拟切换学生
class ChangeStudent implements Runnable{
private Student s;
public ChangeStudent(Student student){
this.s=student;
}
@Override
public void run() {
while(true){
synchronized(s){
if(s.flag){
try {
s.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if(s.getName()=="张三"){
s.setName("李四");
s.setAge(44);
}else{
s.setName("张三");
s.setAge(33);
}
//将标识位置为true,代表下次问问题
s.flag=true;
//唤醒问问题的线程,将其从线程池中取出来
s.notify();
}
}
}
}
//定义一个学生供使用
class Student{
private String name;
private int age;
//标识位
boolean flag=true;//true代表执行问问题,false代表切换学生
//set get方法
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
测试结果
生产者-消费者模型:开辟一条线程代表生产者,开辟一条线程代表消费者,生产者生产后消费者才能消费,消费者消费后生产者才能生产,生产和消费的数量使用随机数,生产者生产的产品数量+剩余的产品数量不能超过1000。
package com.boe.thread;
/**
* 生产者和消费者模型
* 开辟一条线程代表生产者,开辟一条线程代表消费者,生产者生产后消费者才能消费,消费者消费后生产者才能生产
* 生产和消费的数量使用随机数,生产者生产的产品数量+剩余的产品数量不能超过1000
* @author clyang
*
*/
public class ConsumerDemo {
public static void main(String[] args) {
Product p=new Product();
p.setCount(0);
//开一个生产者线程
new Thread(new Producer(p)).start();;
//开一个消费者线程
new Thread(new Consumer(p)).start();;
}
}
//定义生产者
class Producer implements Runnable{
private Product p;
public Producer(Product p){
this.p=p;
}
@Override
public void run() {
while(true){
synchronized(p){
if(!p.flag){
//如果在消费,生产线程先等待
try {
p.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//开始生产,满足生产的产品+上次剩余的不能超过1000
int margin=1000-p.getCount();
//Math.random方法返回范围[0,1)
int productCount=(int)(Math.random()*margin+1);
System.out.println("生产者生产了"+productCount+",上次还剩"+p.getCount()+",总共为:"+(productCount+p.getCount()));
p.setCount(productCount+p.getCount());
//将标识位置为false,代表可以消费了
p.flag=false;
//唤醒消费者线程
p.notify();
}
}
}
}
//定义消费者
class Consumer implements Runnable{
private Product p;
public Consumer(Product p){
this.p=p;
}
@Override
public void run() {
while(true){
synchronized(p){
if(p.flag){
try {
p.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//开始消费,消费的产品数量应该是小于生产完后数量
int consumeCount=(int)(Math.random()*(p.getCount())+1);
//重新设置数量
p.setCount(p.getCount()-consumeCount);
System.out.println("消费者消费了"+consumeCount+",还剩余"+p.getCount()+",总共为:"+(consumeCount+p.getCount()));
//将标识位置为true
p.flag=true;
//唤醒生产者线程
p.notify();
}
}
}
}
//定义一个产品,包含数量
class Product{
//产品数量
private int count;
//设置标识位,true代表生产,false代表消费
boolean flag=true;
//get set方法
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
}
测试结果
notify和notifyAll
参考修改后的生产者-消费模式,上面的代码是有一个问题的,就是锁可能空闲,分析比较复杂,后面再补。
wait和sleep的区别
sleep:可以设定休眠时间,到点了自然醒,如果sleep了有锁也不释放锁,且不释放执行权,没锁就释放执行权,是Thread类的一个静态方法。
wait:可以设定休眠时间,也可不设定休眠时间,如果wait了则释放锁并释放执行权,是Object的一个方法。
线程的状态
新建(NEW):新建一个线程对象
就绪(Runnable):调用了线程的start方法就是就绪状态,能否进入运行状态还要看能否获得CPU时间片
运行(Running):就绪状态的线程获得系统调度,即获得CPU时间片后就进入运行状态
阻塞(Blocked):线程放弃了CPU的使用权,进入阻塞状态
消亡(Dead):线程run方法,或者main方法结束后,进入消亡状态
具体参考博客 https://www.cnblogs.com/hejing-swust/p/8038263.html ,里面有详细图文说明。
守护线程
守护其他线程的线程,在java中分为守护线程和被守护线程,默认为被守护线程。如果被守护线程结束了,守护线程也将结束。GC就是一个守护线程,main方法执行的主线程就是被守护线程。
参考士兵和将军案例,士兵就是守护线程,将军消亡后士兵线程将不再执行。
package com.boe.other;
/**
* 守护线程,士兵和将军
* @author clyang
*/
public class OtherThreadDemo {
public static void main(String[] args) {
Soldier s=new Soldier();
Thread t1=new Thread(s);
Thread t2=new Thread(s);
Thread t3=new Thread(s);
Thread t4=new Thread(s);
//添加士兵为守护线程
t1.setDaemon(true);
t2.setDaemon(true);
t3.setDaemon(true);
t4.setDaemon(true);
//开启线程
t1.start();
t2.start();
t3.start();
t4.start();
for(int i=10;i>0;i--){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
//main方法为被守护线程
System.out.println("将军还有"+i+"滴血");
}
}
}
class Soldier implements Runnable{
@Override
public void run() {
for(int i=1000;i>0;i--){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("士兵还有"+i+"滴血");
}
}
}
测试结果发现,将军消亡后(将军掉血是被守护线程),士兵线程(守护线程)不再继续执行,几乎马上就结束了,如果不设置士兵线程为守护线程,程序一定会执行到士兵掉血到没有才结束。
线程的优先级
线程默认有1-10个优先级,优先级越高抢到资源的概率越大,但是差别不是很大,参考如下案例。
package com.boe.other;
/**
* 线程优先级
* @author clyang
*/
public class ThreadPriority {
public static void main(String[] args) {
PriorityThead p=new PriorityThead();
Thread t1=new Thread(p,"线程1");
Thread t2=new Thread(p,"线程2");
t1.setPriority(5);
t2.setPriority(1);
t1.start();
t2.start();
}
}
class PriorityThead implements Runnable{
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=100;i>0;i--){
System.out.println(Thread.currentThread().getName()+" "+i);
}
}
}
测试结果
总结
以上就是线程入门的基础知识,以窗口卖票为例,可以看出开启的三个线程都需要执行run方法里面的循环卖票逻辑,他们共享一个票数的变量,哪个线程抢到了同步锁,就可以执行卖票,另外没抢到的只能等待,直到下次循环得到锁资源才可以继续卖票。
参考博客
(1)https://blog.csdn.net/qq_22339457/article/details/82386545 线程阻塞
(2)https://www.cnblogs.com/hejing-swust/p/8038263.html 线程的五种状态切换 推荐多读