Java多线程(详细了解java多线程机制)
一、程序、进程、线程
1.1 什么是程序
程序(program):是为完成特定任务、用某种语言编写的一组指令的集合,是一段静态的代码。 (程序是静态的)
1.2 什么是进程
进程(process):是程序的一次执行过程,正在运行的一个程序,进程作为资源分配的单位,在内存中会为每个进程分配不同的内存区域。 (进程是动态的)是一个动的过程 ,进程的生命周期 : 有它自身的产生、存在和消亡的过程
目前操作系统都是支持多进程,可以同时执行多个进程,通过进程ID区分
1.3 什么是线程
线程(thread):进程中的一条执行路径,也是CUP的基本调度单位,一个进程由一个或多个线程组成,彼此间完成不同的工作,多个线程同时执行,称为多线程。
线程的组成
任何一个线程都具有的基本组成部分:
- CPU时间片:操作系统(OS)会为每一个线程分配执行时间。
- 运行数据:堆空间(存储线程需要使用的对象,多个线程可以共享堆中的对象);栈空间(存储线程需要使用的局部变量,每个线程都拥有独立的栈)
线程的特点
- 线程抢占式执行(效率高、可防止单一线程长时间独占CPU)
- 单核CPU在执行的时候,是按照时间片执行的,一个时间片只能执行一个线程,因为时间片特别的短,我们感受到的就是“同时”进行的。
- 多核CPU真正意义上做到了一个时间片多个线程同时进行
- 在单核CPU中,宏观上同时进行,微观上顺序执行
1.4 进程和线程的区别
- 进程是操作系统中资源分配的基本单位,而线程是CPU的基本调度单位
- 一个程序运行后至少有一个进程
- 一个进程可以包含多个线程,但是至少需要有一个线程,否则这个进程是没有意义的
- 进程间不能共享数据段地址,但通进程的线程之间可以。
二、创建线程的三种方式
2.1 继承Thread类重写run()方法
具体实现
1.继承Thread类
2.重写run()方法
3.创建子类对象
4.调用start()方法(PS:不要调用run()方法,这样相当于普通调用对象的方法,并为启动线程)
继承类
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 1; i <= 50; i++) {
System.out.println("子线程:==>" + i);
}
}
}
测试类
public class TestThread {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
for (int i = 1; i <= 50; i++) {
System.out.println("主线程:-->"+i);
}
}
}
结果
获取线程ID和名称
getId()
//获取线程的id,每个线程都有自己的id
getName()
//获取线程名字
Thread.currentThread()
//获取当前线程
代码
public class TestThread {
public static void main(String[] args) {
MyThread t=new MyThread();
t.start();
//只能在继承Thread类的情况下用
System.out.println("线程id:"+t.getId());
System.out.println("线程名字:"+t.getName());
//调用Thread类的静态方法获取当前线程(这里获取的是主线程)
System.out.println("线程id:"+Thread.currentThread().getId());
System.out.println("线程名字:"+Thread.currentThread().getName());
}
}
修改线程名称
只能修改线程的名称,不能修改线程的id(id是由系统自动分配)
1、使用线程子类的构造方法赋值
2、调用线程对象的setName()
方法
代码
public class MyThread extends Thread{
public MyThread() {}//无参构造器
public MyThread(String name) {
super(name);
}
public void run() {
for(int i=1;i<=50;i++) {}
}
}
public class TestThread {
public static void main(String[] args) {
MyThread t1=new MyThread("子线程1");//通过构造方法
MyThread t2=new MyThread();
t2.setName("子线程2");
System.out.println("线程t1的id:"+t1.getId()+" 名称:"+t1.getName());
System.out.println("线程t2的id:"+t2.getId()+" 名称:"+t2.getName());
}
}
2.2 实现Runnable接口实现run()方法
具体实现
1.实现
Runnable
接口
2.实现run()
方法
3.创建实现类对象
4.创建线程类对象
5.调用start()
方法
实现接口
public class MyRunnable implements Runnable{//实现接口
@Override
public void run() {//实现run方法
// TODO Auto-generated method stub
for(int i=1;i<=10;i++) {
System.out.println("子线程:"+i);
}
}
}
测试类
public class TestRunnable {
public static void main(String[] args) {
//1.创建MyRunnable对象,表示线程执行的功能
Runnable runnable=new MyRunnable();
//2.创建线程对象
Thread th=new Thread(runnable);
//3.启动线程
th.start();
for(int i=1;i<=10;i++) {
System.out.println("主线程:"+i);
}
}
}
使用匿名内部类
如果一个线程方法我们只使用一次,那么就不必设置一个单独的类,就可以使用匿名内部类实现该功能
public class TestRunnable {
public static void main(String[] args) {
Runnable runnable=new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
for(int i=1;i<=10;i++) {
System.out.println("子线程:"+ i);
}
}
};
Thread th=new Thread(runnable);
th.start();
}
}
2.3 实现Callable接口
Callable和Thread、Runnable比较
对比继承
Thread
类和实现Runnable
接口创建线程的方式发现,都需要有一个run()
方法,但是这个run()方法有不足:
- 没有返回值
- 不能抛出异常
基于上面的两个不足,在JDK1.5以后出现了第三种创建线程的方式:实现
Callable
接口实现
Callable
接口的好处:
- 有返回值
- 能抛出异常
缺点:
- 创建线程比较麻烦
1.实现
Callable
接口,可以不带泛型,如果不带泛型,那么call方法的返回值就是Object
类型2.如果带泛型,那么call的返回值就是泛型对应的类型
3.从call方法看到:方法有返回值,可以抛出异常
具体实现
实现接口
import java.util.Random;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class TestCallable implements Callable<Integer>{
@Override
public Integer call() throws Exception {
// TODO Auto-generated method stub
return new Random().nextInt(10);
}
}
测试类
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Test {
public static void main(String[] args) throws InterruptedException, ExecutionException {
TestCallable tc=new TestCallable();
FutureTask<Integer> ft=new FutureTask<>(tc);
//创建线程对象
Thread th=new Thread(ft);
th.start();
//获取线程得到的返回值
Integer In=ft.get();
System.out.println(In);
}
}
三、线程的状态
3.1 基本四状态
3.2 等待状态
3.3 阻塞状态
四、线程常用的方法
休眠(当前线程主动休眠millis毫秒)
public static void sleep(long millis)
放弃(当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片)
public static void yield()
加入(当一个线程调用了join方法,这个线程就会先被执行,它执行结束以后才可以去执行其余的线程)
public final void join()
//必须先start(),在join(),才有效优先级(线程优先级为1–10,默认为5,优先级越高,表示获取CPU机会越多)
线程对象.setPriority()
守护线程
- 线程对象.setDaemon(true);设置为守护线程
- 线程有两类:用户线程(前台线程)、守护线程(后台线程)
- 如果程序中所有前台线程都执行完毕了,后台线程也会自动结束
- 垃圾回收器线程属于守护线程
4.1 线程休眠(sleep)
public static void sleep(long millis)
当前线程主动休眠millis毫秒
子线程
public class SleepThread extends Thread{
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
System.out.println(Thread.currentThread().getName()+":"+i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
PS:sleep()的异常在run方法中是不能抛出的,只能用try–catch处理
测试类
public class Test01 {
public static void main(String[] args) {
SleepThread sleepThread = new SleepThread();
sleepThread.start();
}
}
结果:每次间隔100ms输出一次
4.2 线程放弃(yield)
public static void yield()
当前线程主动放弃时间片,回到就绪状态,竞争下一次时间片
子线程
public class YieldThread extends Thread{
@Override
public void run() {
for (int i=1;i<=10;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
Thread.yield();//主动放弃资源
}
}
}
测试类
public class Test01 {
public static void main(String[] args) {
YieldThread yieldThread01 = new YieldThread();
YieldThread yieldThread02 = new YieldThread();
yieldThread01.start();
yieldThread02.start();
}
}
结果:基本都会交替进行,也会有一个线程连续输出
4.3 线程加入(join)
当一个线程调用了join方法,这个线程就会先被执行,它执行结束以后才可以去执行其余的线程,必须先start,再join才有效
子线程
public class JoinThread extends Thread{
@Override
public void run() {
for (int i=1;i<=10;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
测试类
public class Test01 {
public static void main(String[] args) throws InterruptedException {
for (int i=1;i<=10;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
if(i==5){
JoinThread joinThread = new JoinThread();
joinThread.start();
joinThread.join();
}
}
}
}
结果:当主线程打印到5的时候,这时候子线程加入进来,就先执行完子线程,在执行主线程
4.4 守护线程(setDaemon)
将子线程设置为主线程的伴随线程,主线程停止的时候,子线程也不要继续执行了
注意:先设置,在启动
子线程
public class TestThread extends Thread{
@Override
public void run() {
for(int i=1;i<=1000;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
测试类
public class Test01 {
public static void main(String[] args) throws InterruptedException {
TestThread daemonThread = new TestThread();
daemonThread.setDaemon(true);//设置守护线程
daemonThread.start();
for (int i=1;i<=10;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
Thread.sleep(100);
}
}
}
结果:当主线程结束时,子线程也跟着结束,并不会继续执行下去打印输出
4.5 线程优先级(setPriority)
线程优先级为1-10,默认为5,优先级越高,表示获取CPU机会越多
子线程
public class TestThread extends Thread{
@Override
public void run() {
for(int i=1;i<=100;i++){
System.out.println(Thread.currentThread().getName()+":"+i);
}
}
}
测试
public class Test01 {
public static void main(String[] args) throws InterruptedException {
TestThread th1 = new TestThread();
TestThread th2 = new TestThread();
TestThread th3 = new TestThread();
th1.setPriority(10);//设置线程1优先级10
th1.start();
th2.start();//线程2优先级默认不变,为5
th3.setPriority(1);//设置线程3优先级为1
th3.start();
}
}
结果:优先级(th1>th2>th3)线程3应该在最后打印
五、线程安全问题
5.1 卖票案例
需求:模拟三个窗口,每个窗口有100个人,同时抢10张票
使用继承Runnable接口的方法
public class BuyTicketRunnable implements Runnable{
private int ticketNum=10;
@Override
public void run() {
for(int i=1;i<=100;i++) {
if(ticketNum<=0) break;
System.out.println("在"+Thread.currentThread().getName()+"买到了第"+ticketNum+"张票!");
ticketNum--;
}
}
}
测试方法
public class BuyTicketTest {
public static void main(String[] args) {
// TODO Auto-generated method stub
Runnable runnable=new BuyTicketRunnable();
Thread th1=new Thread(runnable,"窗口1");
Thread th2=new Thread(runnable,"窗口2");
Thread th3=new Thread(runnable,"窗口3");
th1.start();
th2.start();
th3.start();
}
}
结果
我们发现,不同窗口会抢到同一张票!!!,这在实际情况是不允许的,这是因为多个线程,在争抢资源的过程中,导致共享的资源出现问题。一个线程还没执行完,另一个线程就参与进来了,开始争抢。(但窗口2抢到第10张票,还没来得及
ticketNum--
操作,时间片就用完了,随后被窗口三抢到CPU资源,此时的票数还是10,窗口三也抢到第十张票,也还没来得及ticketNum--
操作窗口三时间片由完了,窗口一抢到CPU资源,还是买到了第10张票)
多线程安全问题:
- 当多线程并发访问临界资源时,如果破坏原子操作,可能会造成数据不一致
- 临界资源:共享资源(同一对象),一次只能允许一个线程使用,才可以保证其正确性
- 原子操作:不可分割的多步操作,被视作一个整体,其顺序和步骤不可被打乱或缺省
5.2 同步代码块
synchronized
(同步监视器)
- 必须是引用数据类型,不能是基本数据类型
- 也可以创建一个专门的同步监视器,没有任何业务含义 (new Object)
- 一般使用共享资源做同步监视器即可
- 在同步代码块中不能改变同步监视器对象的引用
- 尽量不要String和包装类Integer做同步监视器,建议使用final修饰同步监视器
对卖票案例改进
public class BuyTicketRunnable implements Runnable{
static Object obj=new Object();
private int ticketNum=10;
@Override
public void run() {
for(int i=1;i<100;i++) {
//把具有安全隐患的代码锁住即可,如果锁多了就会效率低
synchronized (obj) {//锁必须多个线程用的是同一把锁!!也可以使用this,表示的是该对象本身
System.out.println("在"+Thread.currentThread().getName()+"买到了第"+ticketNum+"张票!");
ticketNum--;
}
}
}
}
- 多个代码块使用了同一个同步监视器(锁),锁住一个代码块的同时,也锁住所有使用该锁的所有代码块,其他线程无法访问其中的任何一个代码块
- 多个代码块使用了同一个同步监视器(锁),锁住一个代码块的同时,也锁住所有使用该锁的所有代码块, 但是没有锁住使用其他同步监视器的代码块,其他线程有机会访问其他同步监视器的代码块
5.3 同步方法
synchronized
(同步方法)
- 不要将run()定义为同步方法
- 非静态同步方法的同步监视器是this;静态同步方法(static)的同步监视器是 类名.class 字节码信息对象
- 同步代码块的效率要高于同步方法(原因:同步方法是将线程挡在了方法的外部,而同步代码块锁将线程挡在了代码块的外部,但是却是方法的内部)
- 同步方法的锁是this,一旦锁住一个方法,就锁住了所有的同步方法;同步代码块只是锁住使用该同步监视器的代码块,而没有锁住使用其他监视器的代码块
买票案例改进
public class BuyTicketRunnable implements Runnable{
private int ticketNum=10;
@Override
public void run() {
for(int i=1;i<100;i++) {
BuyTicket();
}
}
public synchronized void BuyTicket() {//锁住的是:this,如果是静态方法:当前类.class
if(ticketNum>0) {
System.out.println("在"+Thread.currentThread().getName()+"买到了第"+ticketNum+"张票!");
ticketNum--;
}
}
}
5.4 Lock锁
Lock
锁:
- DK1.5后新增新一代的线程同步方式:Lock锁,与采用synchronized相比,lock可提供多种锁方案,更灵活
- synchronized是Java中的关键字,这个关键字的识别是靠JVM来识别完成的呀。是虚拟机级别的。
但是Lock锁是API级别的,提供了相应的接口和对应的实现类,这个方式更灵活,表现出来的性能优于之前的方式。
对买票案例改进
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class BuyTicketRunnable implements Runnable{
private int ticketNum=10;
Lock lock=new ReentrantLock();//接口=实现类 可以使用不同的实现类
@Override
public void run() {
for(int i=1;i<100;i++) {
lock.lock();//打开锁
try {
if(ticketNum>0) {
System.out.println("在"+Thread.currentThread().getName()+"买到了第"+ticketNum+"张票!");
ticketNum--;
}
}catch(Exception e) {
e.printStackTrace();
}finally {
//关闭锁:--->即使有异常,这个锁也可以得到释放
lock.unlock();
}
}
}
}
Lock和synchronized的区别
- Lock是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁
- Lock只有代码块锁,synchronized有代码块锁和方法锁
- 使用Lock锁,JVM将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)
5.5 线程死锁
- 不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
- 出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续
*案例:男孩女孩一起去吃饭,但是桌子上只有两根筷子,如果两个人同时抢到一根筷子而不放弃,这样两个人都吃不上饭,这样就形成死锁了;必须要有一个人放弃争抢,等待另一个人用完,释放资源,这个人之后才会获得两根筷子,两个人才能都吃上饭 *
package 多线程;
class Eat{
//代表两个筷子
public static Object o1=new Object();
public static Object o2=new Object();
public static void eat() {
System.out.println("可以吃饭了");
}
}
class BoyThread extends Thread{
public void run() {
synchronized (Eat.o1) {
System.out.println("男孩拿到了第一根筷子!");
synchronized (Eat.o2) {
System.out.println("男孩拿到了第二根筷子!");
Eat.eat();
}
}
}
}
class GirlThread extends Thread{
public void run() {
synchronized (Eat.o2) {
System.out.println("女孩拿到了第二根筷子!");
synchronized (Eat.o1) {
System.out.println("女孩拿到了第一根筷子!");
Eat.eat();
}
}
}
}
public class MyLock {
public static void main(String[] args) {
BoyThread boy=new BoyThread();
GirlThread girl=new GirlThread();
boy.start();
girl.start();
}
}
结果
解决办法
先让男孩拿到筷子,线程休眠一下,等待男孩用完筷子,在启动女孩线程
public class MyLock {
public static void main(String[] args) {
BoyThread boy=new BoyThread();
GirlThread girl=new GirlThread();
boy.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
girl.start();
}
}
在写程序中要避免这种死锁:减少同步资源的定义,避免嵌套同步
六、线程通信问题
在Java对象中,有两种池
- 锁池(
synchronized
)- 等待池(
wait()
;notify()
;notifyAll()
)
如果一个线程调用了某个对象的wait方法,那么该线程进入到该对象的等待池中(并且已经将锁释放);
如果未来的某个时刻,另外一个线程调用了相同的对象notify方法或者notifyAll方法,那么该等待池中的线程就会被唤醒,然后进入到对象的锁池里面去获得该对象的锁;
如果获得锁成功后,那么该线程就会沿着wait方法之后的路径继续执行。注意:沿着wait方法之后执行
6.1 wait()和wait(long timeout)
- wait():的作用是让当前线程进入等待状态,同时,wait()也会让当前线程释放它所持有的锁。直到其他线程调用此对象的
notify()
方法或notifyAll()
方法,当前线程被唤醒(进入就绪状态)- wait(long timeout):让当前线程处于“等待(阻塞)状态,直到其他线程调用此对象的
notify()
方法或notifyAll()
方法,或者超过指定的时间量”,当前线程被唤醒(进入就绪状态)
sleep和wait的区别:sleep进入阻塞状态没有释放锁,wait进入阻塞状态但是同时释放了锁
6.2 notify()和notifyAll()
notify()和notifyAll()的作用,则是唤醒当前对象上的等待线程
- notify()是唤醒单个线程
- notifyAll()是唤醒所有的线程
6.3 生产者和消费者问题
案例:
假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费。
如果仓库中没有产品,则生产者将产品放入仓库,否则停止生产并等待,直到仓库中的产品被消费者取走为止。
如果仓库中放有产品,则消费者可以将产品取走消费,否则停止消费并等待,直到仓库中再次放入产品为止。
功能分解一:商品类
public class Product {//商品类
private String name;//名字
private String brand;//品牌
boolean flag = false;//设置标记,false表示商品没有,等待生产者生产
public synchronized void setProduct(String name, String brand) {//生产商品,同步方法,锁住的是this
if (flag == true) {//如果flag为true,代表有商品,不生产,等待消费者消费
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//生产商品
this.setName(name);
this.setBrand(brand);
System.out.println("生产者生产了" +this.getBrand() +this.getName());
//生产完,设置标志
flag = true;
//唤醒消费线程
notify();
}
public synchronized void getProduct() {
if (flag == false) {//如果是false,则没有商品,等待生产者生产
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//如果有商品,消费
System.out.println("消费者消费了" + this.getBrand() +this.getName());
//设置标志
flag = false;
//唤醒线程
notify();
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setBrand(String brand) {
this.brand = brand;
}
public String getBrand() {
return brand;
}
}
功能分解二:生产者线程
public class ProducterThread extends Thread {//生产者线程
private Product p;
public ProducterThread(Product p) {
this.p = p;
}
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
if(i%2==0){//如果是奇数,就生产巧克力,如果是偶数,就生产方便面
p.setProduct("巧克力","德芙");
}else{
p.setProduct("方便面","康师傅");
}
}
}
}
功能分解三:消费者线程
public class CustomerThread extends Thread {//消费者线程
private Product pro;
public CustomerThread(Product pro) {
this.pro = pro;
}
@Override
public void run() {
for (int i = 1; i <= 10; i++) {
pro.getProduct();
}
}
}
功能分解四:测试类
public class Test {
public static void main(String[] args) {
Product p = new Product();
ProducterThread pth = new ProducterThread(p);
CustomerThread cth = new CustomerThread(p);
pth.start();
cth.start();
}
}
结果:生产者生产一件商品,消费者消费一件商品,交替进行
关键词:java 经验分享 java-se intellij-idea,java,多线程,线程,详细,了解,机制
相关推荐
初级JAVA程序员应该知道的ElasticSearch的一个坑
【2021秋招】Java 面试知识点【精华背诵版】
JAVA- 类型和泛型
多线程实现同步机制
面试必问:多线程面试知识点
多线程~再学习
Android的handler基本使用以及做一个简单进度条和轮播图以及子线程中用handler
单例模式中懒汉模式的线程安全问题浅析
单例模式中懒汉模式的线程安全问题浅析
大厂常问:输入URL到显示页面的全过程(敲详细)
JDBC(2) 工具类 | PreparedStatement详细说明
简单详细,树莓派的串口通信设置
你了解前端吗?
了解线段树(C)
软件项目管理0717:开发一定要了解客户
Node.js中模块加载机制
初识Java反射机制 总结
程序人生周排行榜
程序人生月排行榜
1.13 万字 C 语言从入门到精通保姆级教程2021 年版
2.爬虫系列:Scrapy的三种解析方式你都清楚吗?
3.@RequestMapping详解
4.第一章、Python基本数据结构
5.Android Studio设计APP实现与51单片机通过WIFI模块(ESP8266-01S)通讯控制LED灯亮灭的设计源码【详解】
6.开发工具-vscode 使用技巧
7.python经典习题(一)
8.高考是人生旅途的一处驿站
9.样本及抽样分布——《概率论及其数理统计》第六章学习笔记
10.msconfig提示系统配置无法保存原始启动配置以便以后还原的解决方法
11.基于混合策略的麻雀搜索算法
12.最新版Android Studio-2021.11下载、安装步骤(超详细)
13.【毕业季】计算机行业现况的个人分析,请您探讨
14.Python描述 LeetCode 478. 在圆内随机生成点
15.【Java 线程系列】Java线程之间的共享和协作
文章目录
1.1.1 什么是程序
2.1.2 什么是进程
3.1.3 什么是线程
4.1.4 进程和线程的区别
5.2.1 继承Thread类重写run()方法
6.2.2 实现Runnable接口实现run()方法
7.2.3 实现Callable接口
8.3.1 基本四状态
9.3.2 等待状态
10.3.3 阻塞状态
11.4.1 线程休眠(sleep)
12.4.2 线程放弃(yield)
13.4.3 线程加入(join)
14.4.4 守护线程(setDaemon)
15.4.5 线程优先级(setPriority)
16.5.1 卖票案例
17.5.2 同步代码块
18.5.3 同步方法
19.5.4 Lock锁
20.5.5 线程死锁
21.6.1 wait()和wait(long timeout)
22.6.2 notify()和notifyAll()
23.6.3 生产者和消费者问题