java入门基础十一(多线程)

15. 多线程

15.1 进程

进程是系统进行资源分配和调用的独立单元,每一个进程都有它的独立内存空间和系统资源。

15.1.1单进程操作系统和多进程操作系统

单进程操作系统:dos(一瞬间只能执行一个任务)

多进程单用户操作系统:Windows(一瞬间只能执行多个任务)

多进程多用户操作系统:Linux(一瞬间只能执行多个任务)

注:在理论上,现在的多核CPU可以让系统在同一个时刻行多个任务

15.2 线程

15.2.1 什么是线程?

线程是进程里面的一条执行路径,每个线程同享进程里面的内存空间和系统资源

一个进程 可以有 多个线程:各个线程都有不同的分工

15.2.2 线程和进程

  1. 进程和进程之间的关系

    进程之间的内存空间和系统资源是独立的。

  2. 同一个进程里的多条线程

    线程之间的内存空间和系统资源是共享的。

    线程是在进程里的,他们是包含关系

注:

  1. 一个进程里可以有一条或一条以上的线程

  2. 进程里只有一条线程的情况下,这条线程就叫做主线程

  3. 进程里有多条线程的情况下,只有一条线程叫做主线程

15.3 创建多线程

1.线程类

创建MyThread类,继承Thread,重写run方法

public class Test01 {
	
	public static void main(String[] args) {
		
		//创建线程的对象
		MyThread t = new MyThread();
		//启动线程
		t.start();
	}
}
//线程类
class MyThread extends Thread{
	
	//当前线程抢到cpu资源后,就会执行run方法
	@Override
	public void run() {
		System.out.println("当前线程抢到资源了");
	}
}

2.任务类

创建Task类,实现Runnable接口中的run方法

public class Test01 {
	
	public static void main(String[] args) {
		
		Thread t = new Thread(new Task());
		t.start();
	}
}
//任务类
class Task implements Runnable{

	//当前线程抢到cpu资源后,就会执行run方法
	@Override
	public void run() {
		System.out.println("抢到资源了");
	}
}

15.4 感受多线程

需求:编写一个多线程的应用程序,观察其输出的结果,体会多线程互相争抢资源的场景

创建子线程类

public class A extends Thread{

	@Override
	public void run() {
		for (int i = 0; i < 100; i++) {
			System.out.println("A:"+i);
		}
	}
}
public class B extends Thread{

	@Override
	public void run() {
		for (int i = 0; i < 100; i++) {
			Thread.yield();
			System.out.println("B:"+i);
		}
	}
}
public class C extends Thread{

	@Override
	public void run() {
		for (int i = 0; i < 100; i++) {
			System.out.println("C:"+i);
		}
	}
}

测试方法

//创建子线程对象
A a = new A();
B b = new B();
C c = new C();

//开启线程
a.start();
b.start();
c.start();

//主线程打印
for(int i=0;i<100;i++){
    System.out.println("主线程:"+i);
}

观察输出结果,会发现每次运行的结果都不相同,主线程也并非每次都是最先打印,这说明,主线程和子线程,子线程和子线程,都在同时争夺cup资源,而这种争夺是相对随机的,所以,多线程是有不确定性的存在。

注:

  1. 进程 与 进程 的关系:独享内存空间和系统资源

  2. 线程 与 进程 的关系:有一个进程中至少包含一个线程

  3. 线程 与 线程 的关系:在同一个进程里,多个线程共享内存空间和系统资源

  4. 一个进程中可以包含多个线程,但只能有一个主线程

经典面试题:请问当我们编写一个单纯的main方法时,此时该程序是否为单线程的?为什么?

垃圾回收器是一个后台线程

如果子线程之间的任务类似,有多个重复,可以只定义一个线程类,创建多个对象,每一个对象就代表着一条子线程。

示例:

public class MyThread extends Thread {

	public  MyThread(String name) {
		super(name); //调用父类的构造方法,进行线程命名
	}
	@Override
	public void run() {
		for (int i = 0; i < 100; i++) {
			Thread t= Thread.currentThread();  //获取当前线程的对象
			String name = t.getName();         //获取当前对象的名字
			System.out.println(name+":"+i);
		}
	}
}

测试方法

MyThread A = new MyThread("A");
MyThread B = new MyThread("B");
MyThread C = new MyThread("C");

A.start();
B.start();
C.start();
/*
B:0
B:1
......
B:12
C:0
C:1
A:0
A:1
......
*/
//输出顺序相对随机

15.5 Thead中的常用方法

15.5.1 setPriority–设置优先级

需求:在主线程中创3个子线程,并且设置不同优先级,观察其优先级对线程执行结果的”影响”。

示例:

//创建子线程对象
MyThread a = new MyThread("A");
MyThread b = new MyThread("B");
MyThread c = new MyThread("C");

//设置线程对象优先级
a.setPriority(Thread.MAX_PRIORITY);
b.setPriority(Thread.NORM_PRIORITY);
c.setPriority(Thread.MIN_PRIORITY);

//开启线程
a.start();
b.start();
c.start();

通过观察结果发现,这种影响也是不稳定的,但a子线程设置的优先级最高,多次的运行结果表明,a线程先抢到资源的概率稍大,c线程先抢到资源的概率稍小,但也只是略有影响。

15.5.2 set/getName–设置/获取线程名

15.4–感受多线程中,学会使用利用子线程类的构造函数,调用super父类构造方法,对线程名进行设置。

除此之外,还可以使用setName();方法进行手动设置。

示例:

//创建子线程对象。
MyThread A = new MyThread("A");

System.out.println(A.getName());  //A
A.setName("AAAA");
System.out.println(A.getName());  //AAAA

15.5.3 sleep–线程休眠

需求:编写一个随机点名的程序,要求倒数三秒后输出被抽中的姓名

//创建随机数对象
Random random = new Random();
//创建点名数组
String[] names={"小红","小郑","小南","小贝","小东","小西"};

//生成随机数
int nextInt = random.nextInt(names.length);
for(int i=3;i>0;i--){
    System.out.println(i);
    Thread.sleep(1000);   //主线程休眠一秒
}
System.out.println(names[nextInt]);
/*
3
2
1
小东
*/

注:sleep方法一般很少使用。(浪费时间资源和空间资源)

15.5.4 yield–线程礼让

需求:创建两个线程A,B,分别各打印1-100的数字,其中B一个线程,每打印一次,就礼让一次,观察实验结果

子线程类

public  MyThread(String name) {
		super(name);
	}
	@Override
	public void run() {
		for (int i = 0; i < 100; i++) {
			Thread t= Thread.currentThread();
			String name = t.getName();
			if(name.equals("B"))
				t.yield();
			System.out.println(name+":"+i);
		}
	}
}

测试方法

MyThread A = new MyThread("A");
MyThread B = new MyThread("B");

A.start();
B.start();
/*
......
A:10
A:11
B:0
B:1
B:2
B:3
B:4
B:5
B:6
A:12
B:7
A:13
B:8
B:9
......
*/

注:

  1. 并不是B线程只执行一次就轮到A线程执行,而是让前线程退出CPU资源,并转到就绪状态,接着再抢,会出现连续抢到的概率。

  2. yield方法为静态方法,此方法写在哪个线程中,哪个线程就礼让

15.5.5 join–线程合并

需求:主线程和子线程各打印100次,从1开始每次增加1,当主线程打印到10之后,让子线程先打印完再打印主线程

//创建子线程对象
A a = new A();//见15.4 A类

//开启线程
a.start();
for(int i=0;i<100;i++){
    System.out.println("主线程:"+i);
    if(i==10){
        try {
            a.join();   //此方法需要加入受检异常,这里采用try/catch语句。
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
/*
.....
主线程:6
A:0
.....
A:5
A:6
主线程:7
A:7
主线程:8
主线程:9
主线程:10
A:8
A:9
A:10
......
*/
//注:运行结果充满随机

通过结果能够看出在主线程运行到10时,主线程暂停争抢CPU资源,让A子线程先打印结束在打印主线程内容。

15.5.5 线程的中断

方法介绍:

  1. isInterrupted():获取当前线程的状态(true-中断 false-存活)
  2. interrupt(): 改变线程状态

示例:

子线程类

public class MyThread extends Thread {

	@Override
	public void run() {
		
		//isInterrupted() - 获取当前线程状态(true-中断 false-存活)
		while(!Thread.currentThread().isInterrupted()){
			
			System.out.println("111");
			System.out.println("222");
			System.out.println("333");
			System.out.println("444");
		}
	}
}

测试方法

MyThread t = new MyThread();
t.start();
Thread.sleep(3000);
t.interrupt();//改变线程状态

//子线程开启,打印结果,在主线三秒休眠后,调用interrupt方法,改变子线程状态,子线程中断打印结束。

15.5.6 守护线程

守护线程 默默守护着前台线程,当所有的前台线程都消亡后,守护线程会自动消亡

注意:垃圾回收器就是守护线程

守护线程类

public class GuardThread extends Thread{

	@Override
	public void run() {
		
		while(true){
			System.out.println("守护线程~~~~");
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}
}

测试方法

public static void main(String[] args) {
    GuardThread thread = new GuardThread();

    thread.setDaemon(true); 		//将线程置为守护线程
    thread.start();           		//开启线程
    for(int i=0;i<=5;i++){
        System.out.println("主线程~~~"+i);
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}
/*
主线程~~~0
守护线程~~~~
主线程~~~1
守护线程~~~~
守护线程~~~~
主线程~~~2
守护线程~~~~
主线程~~~3
守护线程~~~~
主线程~~~4
主线程~~~5
守护线程~~~~
守护线程~~~~
*/

由结果可以观察到,守护线程在前台线程执行完成之后,就自动销毁,并不需要手动关闭。

15.6 参数传递

15.6.1 传递单个参数

使用ConcurrentHashMap<Thread,参数>集合(ConcurrentHashMap为线程安全的集合)

使用示例:

public class A {

	public void println(){
		Integer num = Test_2.map.get(Thread.currentThread());
		System.out.println(Thread.currentThread().getName()+"   A类的println方法:"+num);
	}
}
public class B {

	public void println(){
		Integer num = Test_2.map.get(Thread.currentThread());
		System.out.println(Thread.currentThread().getName()+"   B类的println方法:"+num);
	}
}
public class Test_2 {

	//ConcurrentHashMap集合传递单个参数。
	public static ConcurrentHashMap<Thread,Integer> map =new ConcurrentHashMap<Thread, Integer>();
	public static void main(String[] args) {
		
		new Thread(new Runnable() {
			public int num=10;
			@Override
			public void run() {
				map.put(Thread.currentThread(), num);
				A a = new A();
				B b = new B();
				a.println();
				b.println();
			}
		}, "线程1").start();
		new Thread(new Runnable() {
			public int num=20;
			@Override
			public void run() {			
				map.put(Thread.currentThread(), num);
				A a = new A();
				B b = new B();
				a.println();
				b.println();				
			}
		}, "线程2").start();
	}
}
/*
线程1   A类的println方法:10
线程1   B类的println方法:10
线程2   A类的println方法:20
线程2   B类的println方法:20
*/

15.6.2 传递多个参数

  1. 依然能够使用使用ConcurrentHashMap集合,value值可以采用数组,数据类,集合。

  2. 但,线程中提供了一种专门的集合ThreadLocal,底层通过map实现,在线程中使用起来比ConcurrentHashMap更加方便。

    示例:

    public class A {
    
    	public void println(){
    		Data data= Test_1.local.get();
    		System.out.println(Thread.currentThread().getName()+"A类的println方法:"+data);
    	}
    }
    
    public class B {
    
    	public void println(){
    		Data data= Test_1.local.get();
    		System.out.println(Thread.currentThread().getName()+"B类的println方法:"+data);
    	}
    }
    
    package com.dream.test01;
    
    public class Data {
    	private int num;
    	private String name;
    	
        //构造方法修饰为private,不允许外部创建对象
    	private Data(int num,String name){
    		this.name=name;
    		this.num=num;
    	}
    
    	public int getNum() {
    		return num;
    	}
    
    	public void setNum(int num) {
    		this.num = num;
    	}
    
    	public String getName() {
    		return name;
    	}
    
    	public void setName(String name) {
    		this.name = name;
    	}
        
        //通过方法setDate创建对象,保证每个线程只能创建一个data对象。
    	public static Data setData(int num,String name){
    		Data data = Test_1.local.get();
    		if(data==null){
    			return new Data(num,name);
    		}
    		else{
    			data.setName(name);
    			data.setNum(num);
    		}
    		return data;
    	}
    
    	@Override
    	public String toString() {
    		return "Data [num=" + num + ", name=" + name + "]";
    	}
    	
    }
    
    public class Test_1 {
    
    	//ConcurrentHashMap集合传递单个参数。
    	public static ConcurrentHashMap<Thread,Integer> map =new ConcurrentHashMap<Thread, Integer>();
    	//使用ThreadLocal传递多个参数(参数放在数据类中)
    	public static ThreadLocal<Data> local= new ThreadLocal<>();
    	public static void main(String[] args) {
    		
    		new Thread(new Runnable() {
    			public int num=10;
    			public String name="线程1";
    			@Override
    			public void run() {
    				local.set(Data.setData(num, name));
    				A a = new A();
    				B b = new B();
    				a.println();
    				b.println();
    			}
    		}, "线程1").start();
    		new Thread(new Runnable() {
    			public int num=20;
    			public String name="线程2";	
    			@Override
    			public void run() {			
    				local.set(Data.setData(num, name));
    				A a = new A();
    				B b = new B();
    				a.println();
    				b.println();				
    			}
    		}, "线程2").start();;
    	}
    
    }
    /*
    线程2A类的println方法:Data [num=20, name=线程2]
    线程1A类的println方法:Data [num=10, name=线程1]
    线程1B类的println方法:Data [num=10, name=线程1]
    线程2B类的println方法:Data [num=20, name=线程2]
    */
    

15.7 线程的生命周期

1、新建状态

i. 在程序中用构造方法创建了一个线程对象后,新的线程对象便处于新建状态,此时,它已经有了相应的内存空间和其它资源,但还处于不可运行状态。新建一个线程对象可采用线程构造方法来实现。

ii. 例如:Thread thread=new Thread();

2、 就绪状态

i. 新建线程对象后,调用该线程的start()方法就可以启动线程。当线程启动时,线程进入就绪状态。此时,线程将进入线程队列排队,等待CPU调用,这表明它已经具备了运行条件。

3、运行状态

i. 当就绪状态的线程被调用并获得处理器资源时,线程就进入了运行状态。此时,自动调用该线程对象的run()方法。run()方法定义了该线程的操作和功能。

4、 阻塞状态

i. 一个正在执行的线程在某些特殊情况下,如被人为挂起,将让出CPU并暂时中止自己的执行,进入阻塞状态。在可执行状态下,如果调用sleep(2000)、wait()等方法,线程都将进入阻塞状态。阻塞时,线程不能进入排队队列,只有当引起阻塞的原因被消除后,线程才可以转入就绪状态。

5、死亡状态

i. 线程调用stop()方法时或run()方法执行结束后,线程即处于死亡状态。处于死亡状态的线程不具有继续运行的能力。

15.8 线程安全

线程安全的三种方式:

1.局部加锁

局部加锁采用synchronized关键字

结构:

synchronized(Object){

​ …需要加锁的代码块…

}

含义:进入代码块自动加锁,出代码块自动开锁。

注:在线程类中Object应该使用所有类对象共有的对象,如:static修饰的静态类变量(private static Object obj = new Object();),字符串常量,类的.class文件对象。

在任务类中应该使用各自对象拥有的类,如:成员变量,this。

2.方法加锁

采用synchronized修饰方法。

结构:

public(访问修饰符) synchronized void(返回类型) method(方法名){

​ …方法代码块…

}

进入方法自动加锁,退出方法,自动开锁。

注:该方法有static修饰时,锁对象为类的字节码文件对象(线程类使用),无static修饰,锁对象只有调用的本对象(任务类使用)。

3.手动加锁lock()/unlock()

结构:

Lock lock =new  ReentrantLock();
public void run() {
    lock.lock();  	//手动上锁
        //...需要加锁的代码块...
    lock.unlock();  //手动关锁
}

注:在线程类中应该使用static修饰lock为静态变量,而在任务类中lock则不应该被修饰为静态变量。

​ 原因:

​ 子线程类中每创建一个线程就需要新new一个线程对象,需要lock锁能够锁住该线程类创建的所有线程对象,需要使用static静态修饰,如果不使用静态修饰,该锁将会无任何意义。

​ 而在任务类中,因为任务类对象在创建对象之后,只需要加入新new的线程类,所以在上锁时,是以对象为单位,如果使用static修饰,该任务类的所有对象都将要共享这一个锁,会导致该任务类的其他任务对象也被锁。

详细示例可以见 拓展:多线程模拟售票。

拓展示例:

1.计算任务,一个包含了2万个整数的数组,分拆了多个线程来进行并行计算,最后汇总出计算的结果。

子线程类

public class MyThread extends Thread{

	private int startindex;
	private int endindex;
	private int [] array;
	private int sum;
	
	public MyThread(String name,int startindex, int endindex, int[] array) {
		super(name);
		this.startindex = startindex;
		this.endindex = endindex;
		this.array = array;
	}
	public int add(){
		return sum;
	}
	@Override
	public void run() {
		for (int i = startindex; i <endindex; i++) {
			sum+=array[i];
			System.out.println(Thread.currentThread().getName()+"正在计算.....");
		}
	}	
}

测试方法:

public static void main(String[] args) throws InterruptedException {
    int [] array=new int[20000];
    for(int i=0;i<20000;i++){
        array[i]=i+1;
    }
    MyThread A = new MyThread("A",0,5000,array);
    MyThread B = new MyThread("B",5000,10000,array);
    MyThread C = new MyThread("C",10000,15000,array);
    MyThread D = new MyThread("D",15000,20000,array);

    A.start();
    B.start();
    C.start();
    D.start();

    A.join();
    B.join();
    C.join();
    D.join();

    System.out.println(A.add()+B.add()+C.add()+D.add());
}
/*
......
200010000
*/

2.铁道部发布了一个售票任务,要求销售1000张票,要求有3个窗口来进行销售,请编写多线程程序来模拟这个效果。

i. 窗口001正在销售第1000张票

ii. 窗口001正在销售第999张票

iii. 窗口002正在销售第998张票

iv. 。。。

v. 窗口002正在销售第1张票

涉及到线程安全,要加锁。

线程类:

public class SellTicket extends Thread{
	//设置总票数
	private static int tickets=1000;
	
	
	private static String str = new String ("start");
	
	public SellTicket(String name){
		super(name);
	}
	@Override
	public void run() {
		while(tickets > 0)
        {
            synchronized(str)//同步代码块
            {
                if(tickets > 0)
                {
                    System.out.printf ("%s窗口正在售出第%d张票\n",Thread.currentThread().getName(),tickets);
                    tickets--;
                }
                if(tickets<=0){
                	System.out.printf("%s窗口售罄\n",Thread.currentThread().getName());
                }
            }
        }
	}
}

测试方法:

public static void main(String[] args) {
    SellTicket A = new SellTicket("001");
    SellTicket B = new SellTicket("002");
    SellTicket C = new SellTicket("003");
    A.start();
    B.start();
    C.start();
}
/*
......
002窗口正在售出第2张票
002窗口正在售出第1张票
002窗口售罄
003窗口售罄
001窗口售罄
*/

详细示例可以见 拓展:多线程模拟售票。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值