Java多线程(从底层理解)

本文详细介绍了Java多线程的基本概念,如线程和进程的关系,线程生命周期、start方法与run方法的区别,创建线程的不同方式,线程同步与互斥,以及关键方法如sleep、yield、join和wait/notify。通过实例演示,深入理解线程调度与协作机制。

Java多线程

1.线程和进程的概念

进程:程序的一次执行。
线程:一个进程中的执行单元。

2.线程和进程的关系

我们可以将进程理解为一个工厂那么线程就是工人。
那么一个进程就可以拥有很多个线程。

3.线程的生命周期

大家可以先简单看下这张生命周期图

在这里插入图片描述

4.start方法 和 run方法的作用和区别

①.如果你直接调用run方法则相当于在主线程中调用一个普通对象的普通成员方法,并不会开启线程
①.调用start方法则会让线程进入就绪状态并且执行run方法。(可以参考上面的生命周期图)

5.线程的创建方式

①.继承的方式:通过继承Thread并且重写run方法。
代码如下:

package CSDN;

public class MyThread extends Thread {
    public void run(){

        }
    }
}

主方法中先生成对象然后启动线程:

package CSDN;

public class Entrance {
    public static void main(String[] args) {

        MyThread myThread=new MyThread();
        myThread.start();

    }
}

①.通过实现Runnable接口: 重写run方法并将生成的对象通过Thread的构造方法传给相应的thread对象.

代码如下:

package CSDN;

public class MyRunnable  implements Runnable{
    public void run(){

    }
}

主方法:

package CSDN;

public class Entrance {
    public static void main(String[] args) {
      MyRunnable myRunnable=new MyRunnable();
      Thread thread=new Thread(myRunnable);
      thread.start();
    }
}

6.线程间是如何运行的?

示例代码:

子线程类:

package CSDN;

public class MyThread extends Thread {
    public void run(){
        for(int i=0;i<30;i++){
            System.out.println("子线程 "+i);
        }
    }
}

主方法类:

package CSDN;

public class Entrance {
    public static void main(String[] args) {
        MyThread myThread=new MyThread();
        myThread.start();
        for(int i=0;i<30;i++){
            System.out.println("主线程"+i);
        }
    }
}

运行结果的部分截图:
在这里插入图片描述
在这里插入图片描述
从运行结果来看主线程和子线程Mythread有很多种运行情况我们可以总结一下:
第一种:主线程和子线程交错运行.
第二种:子线程多打印几行然后又轮到主线程打印
第三钟:主线程多打印几行代码然后又轮到子线程打印

理解:
首先java虚拟机中有一个调度中心里面都是一些处于就绪状态的线程们,调度中心会随机给处于就绪状态的线程们分配时间片(时间片可以理解为资源),然后拿到时间片的线程就可以先执行, 当拿到的时间片用完后这个线程就会回到调度中心,调度中心再将时间片随机分配给一个线程(当然有可能还是这个线程拿到接着运行)。

用这个思路我们一起来分析一下主方法中的代码:

第一种情况:当我们运行主方法后开启了主线程,调度中心分配给主线程的时间片长度只够运行到 myThread.start(); 此时开启了mythread线程。那么此时调度中心就有俩个处于就绪状态的线程主线程和mythread线程,然后调度中心再随机分配给时间片给mythread或者主线程去运行。

第二种情况:当我们运行主方法后主线程就拿到了时间片先运行,调度中心分配给主线程的时间片长度可以运行到最后一行代码,那么主线程就可以直接开始打印。

主线程或者myThread线程连续打印:调度中心连续选中主线程或者myThread线程并分配时间片连续运行,其他的运行情况的思路都是一样就不一 一解释了。

7.synchronized关键字—锁

①.对象锁

a.当使用对象锁的时候,注意要是相同的对象,并且当有线程正在访问对象锁内部的代码的时候,其他线程无法访问。(注意无法访问的范围)
b.但是并不影响没有使用对象锁的部分的代码的运行。

对象锁分为两类一个叫做synchronized代码块(圆括号内是普通类的对象),另外一个是sybchronized修饰普通成员方法。它们二者其实可以通过this关键字进项转化。

第一类对象锁示例代码:
第一个子线程类:

package CSDN;

public class MyThread1 extends Thread {
    Object obj;

    void setObject(Object obj){
        this.obj=obj;
    }

    public void  run(){
        synchronized (obj){
            for (int i=0;i<10;i++){
                System.out.println("子线程1 "+i);
            }
        }
    }
}

第二个子线程类:

package CSDN;


public class MyThread2 extends Thread{
    Object obj;

    void setObject(Object obj){
        this.obj=obj;
    }

    public void  run(){
        synchronized (obj){
            for (int i=0;i<10;i++){
                System.out.println("子线程2 "+i);
            }
        }
    }
}

主方法类:

package CSDN;

public class Entrance {
    public static void main(String[] args) {
     Object obj=new Object();

       MyThread1 myThread1=new MyThread1();
       MyThread2 myThread2=new MyThread2();

        myThread2.setObject(obj);
        myThread1.setObject(obj);

        myThread2.start();
         myThread1.start();

    }
}

运行结果:
在这里插入图片描述
解析
首先我们实例化一个Object对象并且给子线程1和子线程2的run();方法添加了synchronized(object)这个锁,它代表的意思是只有拿到这个object对象才可以运行run方法中的代码。

运行结果分析:首先我们不能保证子线程1和子线程2哪个先运行并且我只创建了一个object对象。假设调度中心分配给主线程的时间片运行到开启俩个子线程就结束了,此时调度中心只剩下子线程1和子线程2,然后调度中心先选中子线程1执行这个时候子线程1发现执行run方法时需要一个object对象,那么子线程1就先抢到了这个object对象假设调度中心分配给子线程1的时间片只够打印五行,打印五行结束后子线程1又回到调度中心。调度中心下一个选中了子线程二,子线程二准备执行代码时候发现需要一个object对象才可以执行,但是object对象已经被子线程1抢走了所以子线程2只能退回调度中心重新选择。依此反复,只有当子线程1运行完毕释放了object对象,子线程2拿到object对象才可以运行。所以我们就不会看到俩个子线程交错运行的情况了,只能看到一个线程运行完毕另一个线程才可以运行。

第二类对象锁::
子线程1:

package CSDN;

public class Thread1 extends Thread{
    Lock lock;

    void setLock(Lock lock){
        this.lock=lock;
    }

    public void run(){
        lock.test();
    }

}

子线程2:

package CSDN;

public class Thread2 extends  Thread{
    Lock lock;
    void setLock(Lock lock){
        this.lock=lock;
    }
    public void run(){
        lock.test();
    }
}

Lock类:

package CSDN;

public class Lock {
    void test(){
                //this
        synchronized (this){
            for (int i=0;i<10;i++){
                System.out.println(this+"子线程-"+i);
            }
        }
   }
}

主类:

package CSDN;

public class Entrance {
    public static void main(String[] args) {
          Lock lock=new Lock();
          Thread1 thread1=new Thread1();
          Thread2 thread2=new Thread2();
          thread1.setLock(lock);
          thread2.setLock(lock);
          thread1.start();
          thread2.start();

    }
}

运行结果:
在这里插入图片描述

解析
不难发现第二类对象锁其实就将object对象换为了this关键字,代表俩个子线程中谁先调用test方法就获得了lock这个对象就可以先运行。运行结果分析和上面相同。

②.类锁

a. 当使用类锁的时候,只要是同一个类的对象.当有线程正在访问类锁内部的代码的时候,其他线程无法访问。(注意无法访问的范围)
b. 但是并不影响没有使用类锁的部分的代码的运行
对象锁分为两类一个叫做synchronized代码块(圆括号内是class对象),另外一个是sybchronized修饰静态成员方法。它们二者其实可以通过class对象进项转化。

注意: 类锁和对象锁之间没有关系.

代码示例:
子线程1:

package CSDN;

public class Thread1 extends Thread{
    Lock lock=new Lock();

    public void run(){
        lock.test();
    }

}

子线程2:

package CSDN;

public class Thread2 extends  Thread{
   Lock lock=new Lock();

    public void run(){
        lock.test();
    }
}

Lock类:

package CSDN;

public class Lock {
    void test(){
                        //类锁
        synchronized (Lock.class){
            for (int i=0;i<10;i++){
                System.out.println("Lock"+i);
            }
        }
   }
}

运行结果:
在这里插入图片描述
分析:将synchronized()括号里面换成一个类名,也就是说俩个子线程谁先调用了test();方法就获得了这个类就可以先执行完毕。

另一种写法:

package CSDN;

public class Lock {
   static synchronized void test(){
            for (int i=0;i<10;i++){
                System.out.println("Lock"+i);
            }

   }
}

解析
用static和synchronized修饰一个方法相当于这个方法被类锁包括,就和是上面第一种写法作用一样。
我们知道被static修饰的方法有俩种调用方式,第一种是使用类名直接调用第二种是实例化一个对象来调用,不管使用哪种调用方式调用这个方法都会拿到类锁进行运行

8.sleep方法

①会让当前线程进入阻塞状态,但是不会释放锁.
②.会丢弃当前剩余的时间片,立马进入阻塞状态
③.在调用sleep方法的那个线程中起作用
④.sleep方法不会释放锁

代码示例:

子线程类:

package CSDN;

public class Thread1 extends Thread{

    public void run(){
        for (int i=0;i<10;i++){
            System.out.println("子线程-"+i);
        }
    }

}

主类:

package CSDN;

public class Entrance {
    public static void main(String[] args) throws InterruptedException {
          Thread1 thread1=new Thread1();
          thread1.start();
          Thread.sleep(1000);
          for (int i=0;i<10;i++){
              System.out.println("主线程-"+i);
          }
          
    }
}

运行结果:
在这里插入图片描述
解析
sleep();方法有一个特点就是你在哪个线程中调用这个方法哪个线程就会休眠
上面结果我们可以这样分析,假设开始运行时调度中心分配给主线程一个可以运行完主方法的时间片,但是你在主线程中调用了sleep方法主线程就会丢弃剩余的时间片进入阻塞状态,调度中心里此时就只剩下了子线程那接下来调度中心就只能选中子线程去运行,结果就是子线程先运行完。

②.sleep();方法还有一个特点就是不会释放锁
代码示例:
子线程1类:

package CSDN;

public class Thread1 extends Thread {
    Object obj;

    void setObject(Object obj) {
        this.obj = obj;
    }

    public void run() {
        synchronized (obj) {
            for (int i = 0; i < 5; i++) {
                System.out.println("线程1-" + i);
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            for (int i = 6; i< 10; i++) {
                System.out.println("线程1-" + i);
            }
        }
    }

}




子线程2类:

package CSDN;
public class Thread2 extends  Thread{
    Object obj;
    void setObj(Object obj){
        this.obj=obj;
    }

    public void run(){
        synchronized (obj){
            for(int i=0;i<5;i++){
                System.out.println("线程2-"+i);
            }
        }
    }
}

主类:

package CSDN;

public class Entrance {
    public static void main(String[] args)  {
          Object object=new Object();
          Thread1 thread1=new Thread1();
          Thread2 thread2=new Thread2();

          thread1.setObject(object);
          thread2.setObject(object);

          thread1.start();
          thread2.start();
    }
}

运行结果:
在这里插入图片描述运行现象是子线程1先打印0–4 然后休眠一秒再打印子线程2的代码
解析
首先子线程1先调用了setobject方法拿到了object这个对象锁- - -假设调度中心先选中子线程一开始运行- - -运行了一会发现子线程1中调用了sleep方法- - -子线程1就进入堵塞状态并丢弃当前剩余时间片- - -然后调度中心下一个选中了子线程2- - -发现子线程2准备运行时需要object这个对象锁- - -但object对象已经被子线程1掳走了- - -导致子线程2也执行不了- - -所以只能等子线程1休眠结束并完全运行完成后子线程2才可以运行。说明调用sleep方法虽然会休眠一段时间但并不会释放锁

9.yield方法

①.会让当前线程暂停执行,进入就绪状态
②. 会丢弃当前剩余的时间片,立马回到调度中心
这个方法并不常用只有当你设置线程优先级时候会有用处,更好的理解我们可以将这个方法理解为礼让,它和sleep方法调用方式相同在当前线程中调用

10.join方法

①.会让产生线程进入阻塞状态,直到相应线程执行完毕
②. 会丢弃当前剩余的时间片,立马进入阻塞状态
③.join方法和上面俩个方法的区别是,它不是静态方法所以需要实例化对象去调用。
④.在哪个线程中运行join方法就作用于哪个线程.

代码示例:
子线程:

package CSDN;

public class Thread1 extends Thread {
    public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println("线程1-" + i);
            }
    }

}


主类:

package CSDN;

public class Entrance {
    public static void main(String[] args) throws InterruptedException {
          Thread1 thread1=new Thread1();
          thread1.start();
          thread1.join();
        for(int i=0;i<10;i++){
            System.out.println("main-"+i);
        }
    }
}

运行结果:
在这里插入图片描述
解析
首先我们在主方法中调用了join方法主线程就会进入阻塞状态- - -此时调度中心就只剩下了子线程- - -调度中心就分配时间片给子线程进行打印- - -直到子线程打印完毕才会轮到主线程打印。

11.wait和notify/notifyAll方法

线程间的通讯。一个线程必须在拿到一个对象锁的情况下调用这个对象的wait()方法进入休眠的状态并且释放锁,当另一个线程也拿到这个锁时可以调用这个对象的notify/notifyAll一个或者全部休眠线程回到就绪状态(调度中心),然后如果休眠的线程拿到锁和时间片将会从上次休眠的代码处继续执行

一个线程间通讯的代码示例:
消耗线程类:

package Class27Item;

class ConsumerThread extends Thread{
	Test test;
	
	void setTest(Test test){
		this.test=test;
		
	}
	
	
	public void run(){
		test.consume();
	}
	
	
	
}

生产线程:

package Class27Item;

class GeneratorThread extends Thread{
	Test test;
	
	void setTest(Test test){
		this.test=test;
		
	}
	
	public void run(){
		test.generate();

	}
	
	
	
}

test方法类:

package Class27Item;

class Test{
	private Object object;
	int i=10;
	Test(Object object){
		this.object=object;
		
		
	}
	
	
	void generate(){
		synchronized(object){
				try{
					while(i<5){
						i++;
					}
					System.out.println("1--->");
					object.notifyAll();
				}catch(Exception e){
					
					
				}
				
		}
		

			
		}
		
		
		
	
	
	void consume(){
		synchronized(object){
			try{
				i--;
				while(i<5){
					System.out.println("i值太小,等待");
					object.wait();
					System.out.println("i的值不小了,复活1");
					System.out.println("i的值不小了,复活2");
					System.out.println("i的值不小了,复活3");

				}	
				
			}catch(Exception e){
				
				
			}

			
		}
	}
	

}

主类:

package Class27Item;

class Entrance{
	public static void main(String[] args)throws Exception{
		Object obj=new Object();
		Test t=new Test(obj);
		
		ConsumerThread[] cts=new ConsumerThread[10];
		for(int i=0;i<10;i++){
			cts[i]=new ConsumerThread();
			cts[i].setTest(t);
			
		}
		
		for(int i=0;i<10;i++){
			cts[i].start();
			
		}
		
		
		Thread.sleep(3000);
		GeneratorThread gt=new GeneratorThread();
		gt.setTest(t);
		gt.start();
			

		
	}
	
	
	
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值