线程

多线程

  • 一、进程、线程
    • 1.1 进程
    • 1.2 线程
    • 1.3 java程序的进程和线程
    • 1.4 主线程、子线程
  • 二、多线程
    • 2.1 单核cup可以做到多线程并发吗?
    • 2.2 实现多线程方式
      • 2.2.1 子类继承父类(Thread)
      • 2.2.2 start()方法
      • 2.2.3 实现Runnable接口
      • 2.2.4 采用匿名内部类
      • 2.2.5 实现callable接口
    • 2.3 获取和修改线程的名字
  • 三、 线程生命周期
    • 3.1 新建状态
    • 3.2 就绪状态
    • 3.3 运行状态
    • 3.4 死亡状态
    • 3.5 阻塞状态
      • 3.5.1 线程sleep(休眠方法)
      • 3.5.2 线程interrupt("叫醒线程方法")
      • 3.5.3 线程stop(强行中断线程)
  • 四、线程的调度
    • 4.1 常见的调度模型
    • 4.2 java中的线程调度方法
  • 五、多线程安全(重点)
    • 5.1 什么时候会出现多线程并发的安全问题
    • 5.2 解决方法(线程同步机制)
    • 5.3 变量的线程安全问题
    • 5.4 死锁
  • 六、 守护线程
    • 6.1 守护线程的实现
    • 6.2 定时器
  • 七、notify和wait方法
    • 7.1 wait方法
    • 7.2 notify方法
    • 7.3 两种方法的使用

一、进程、线程

java语言之所以有多线程,目的是提高程序的运行效率。

1.1 进程

 进程是一个应用程序(一个进程是一个软件)。

1.2 线程

线程是一个进程中的执行场景、执行单元。一个进程可以启动多个线程

1.3 java程序的进程和线程

对于java程序来说,当在DOS命令窗口中输入:
java HelloWorld 回车之后。会先启动JVM,而JVM就是一个进程。

JVM再启动一个主线程调用主方法(main),同时在启动一个垃圾回收的线程负责看护,回收垃圾。

所以,目前java程序至少有2个线程并发,一个是执行main方法的主线程。一个是垃圾回收线程。

两个进程内存独立不会共享,同一个进程的两个线程堆内存和方法区共享,但是栈内存独立,一个线程一个栈。

假设有10个线程就有十个栈。每个栈直接互不干扰,各自执行,这就是多线程并发

1.4 主线程、子线程

主线程结束了,子线程还会执行吗?

	Thread t1=new Thread(new Runnable() {
     
			
			
			public void run() {
     
				for (int i = 0; i <= 100; i++) {
     
					if(i==100) {
     
						System.out.println("子线程执行结束了!");
					}
					
				}
				
			}
		});
		
		t1.start();
		System.out.println("主线程执行结束了!");

运行结果:
主线程执行结束了!
子线程执行结束了!
线程_第1张图片

通过测试可以看出主线程结束了,子线程仍然还在运行

有没有函数可以让主线程等子线程结束了才结束运行主线程
有的。
通过在主线程的任务中用子线程调用join()就可。
join解释成:等待线程死亡。

public static void main(String[] args) throws InterruptedException {
     
		test t2=new test();
		
		t2.start();
		t2.join();
		
		System.out.println("主线程执行");

	}

}
class test extends Thread{
     
	public void run() {
     
		System.out.println("子线程执行");
	}
}

输出结果:
子线程执行
主线程执行

二、多线程

2.1 单核cup可以做到多线程并发吗?

什么是真正的多线程并发?对于多核cpu电脑真正的多线程并发肯定没有问题。

t1线程执行t1的,t2线程执行t2的,t1不会影响t2的,t2也不会影响t1的,这就做真正的多线程并发。
单核的cup只有一个‘大脑’,不能做到真正的多线程并发,但是可以给人一种多线程并发的感觉。
对于单核的cup来说,在某个时间点上实际只能处理一件事情,由于cup速度极快,多个线程频繁切换,给人造成多
线程并发的错觉。

举个例子:

网上最近很火的小书,通过大拇指的松动给人一种画面在动的错觉。

还有很著名的”膝跳反应原理“,拿小锤在你膝盖上敲,到感到痛觉,对于人的反应来说已经很快了,觉得就是同步的,其实不是,它会有个反应时间,这个反应时间对于我们人来说感觉不到,但是对于计算机来说,可以进行数亿次运算了,人类的大脑就好比单核计算机的cpu。

2.2 实现多线程方式

2.2.1 子类继承父类(Thread)

直接通过子类去继承父类(Thread),重写里面的run()方法
public static void main(String[] args) {
     
		//main方法在主线程中,在主栈运行
		//创建一个线程的对象
		MyThread myThread=new MyThread();
		
		//启动线程
		myThread.start();
		
		for (int i = 0; i < 10; i++) {
     
			System.out.println("主线程----->"+i);
		}

	}

}
class MyThread extends Thread{
     
	@Override
	public void run() {
     
//		编写程序,运行在分支线程(栈)中
		for (int i = 0; i < 10; i++) {
     
			System.out.println("分支线程---->"+i);
		}
		
	}
}

2.2.2 start()方法

start()方法的作用是:在JVM中开辟一块新的栈空间,开启后start()方法瞬间结束。
只要空间开出来,线程就启动成功了,分支线程自动调用run()方法执行程序,并且start()方法会在分支栈的最底部,和主线程的main方法差不多。
线程_第2张图片
下面代码有什么区别?

myThread.run();
myThread.start();

直接调用run方法,分支栈并没有开辟出来,所以还是单线程。
而调用start方法,分支栈开辟出来,启动了多线程。

2.2.3 实现Runnable接口

Runnable并不是一个线程类,而是一个可运行的类,在主线程中创建一个可运行的对象,将可运行的对象封装成一个线程对象。

public static void main(String[] args) {
     
		
		MyRunnable myRunnable=new MyRunnable();
		Thread thread=new Thread(myRunnable);
		thread.start();
		
		for (int i = 0; i < 10; i++) {
     
			System.out.println("主线程----->"+i);
		}

	}

}

class MyRunnable implements Runnable{
     

	
	public void run() {
     
//		编写程序,运行在分支线程(栈)中
		for (int i = 0; i < 10; i++) {
     
			System.out.println("分支线程---->"+i);
		}
		
	}

2.2.4 采用匿名内部类

public static void main(String[] args) {
     


		
		Thread t=new Thread(new Runnable() {
     
			
			
			public void run() {
     
				System.out.println("支线程运行");
				
			}
		});
		
		t.start();
		System.out.println("主线程执行");

	}

2.2.5 实现callable接口

众所周知,前面两种线程不会有返回值,还有另一种线程的实现方法可以有返回值
public static void main(String[] args) throws InterruptedException, ExecutionException {
     
		// 创建未来任务对象
		FutureTask <Integer>future=new FutureTask<Integer>(new Callable<Integer>() {
     
		
			
			public Integer call() throws Exception {
     
				int a=10;
				int b=20;
				return a+b;
			}
		});
		
		
			Thread t1=new Thread(future);
			t1.start();
			
			System.out.println(future.get());
	}

小结:这种线程实现的方式可以获取到线程结束后返回的值,call方法类似于run方法,不同的是:call方法会有返回值。

这让我想起来前一段时间的一道考试题,这道题我就卡在了不能获取线程的返回结果,学习到这,我重新想到那题的解决办法:

问题是:创建2个线程,一个线程求100内的偶数,一个线程求100内余数,并求和。
public static void main(String[] args) throws InterruptedException, ExecutionException {
     
	    	
	    	
	    	FutureTask<Integer> future=new FutureTask<Integer>(new Callable<Integer>() {
     
			
	    	
	    	public Integer call() throws Exception {
     
	    		int num=0;
	    		for (int i = 1; i <=100; i++) {
     
					if(i%2==0) {
     
						num+=i;
					}
				}
	    		return num;
	    		
	    	}
	    	
	    	
	    	});
	    	
	    	FutureTask<Integer> future2=new FutureTask<Integer>(new Callable<Integer>() {
     
				
		    	
		    	public Integer call() throws Exception {
     
		    		int num2=0;
		    		for (int i = 1; i <=100; i++) {
     
						if(i%2!=0) {
     
							num2+=i;
						}
					}
		    		return num2;
		    		
		    	}
		    	
		    	
		    	});
	    	
	    	Thread t1=new Thread(future);
	    	Thread t2=new Thread(future2);
	    	t1.start();
	    	t2.start();
	    	System.out.println(future.get()+future2.get());

小结:这种方法优点是:可以获取到线程的返回值。缺点是,效率低,在获取线程返回值的时候,当前线程会阻塞。

2.3 获取和修改线程的名字

线程有默认的名字 ---->Thread-0
可以通过setName方法进行修改线程名字。

获取到当前线程对象
Thread t=Thread.currentThread();
返回值t就是当前线程。如果出现在主线程中当前线程就是主线程。

三、 线程生命周期

线程_第3张图片

3.1 新建状态

新建状态是新new出来的线程对象。

3.2 就绪状态

就绪状态又叫可运行状态,表示当前的线程具有抢夺cpu时间片的权力(CPU时间片就是执行权)。
当一个线程抢夺到cpu时间片之后,就会开始执行run方法,run方法执行代表着线程进入运行状态。

3.3 运行状态

run方法开始执行标志着这个线程进入运行状态,当之前占有的cpu时间片用完之后,会重新回到就绪状态继续抢夺cpu时间片,当再次抢到cpu时间片之后,会重新进入run方法接着上一次的代码继续往下执行。

3.4 死亡状态

当run方法执行完,线程死亡。

3.5 阻塞状态

当一个线程进入到阻塞事件,例如接受用户的键盘输入,或者sleep方法等,此时线程会进入阻塞状态,阻塞状态的线程会放弃之前占有的
cpu时间片。重新进入到就绪状态继续抢cpu时间片。

3.5.1 线程sleep(休眠方法)

static void sleep(long millis);

sleep是一个静态的方法,参数是毫秒,作用是让当前线程进入休眠,进入’阻塞状态‘,放弃占有的cpu时间片,让给其他线程使用。

public static void main(String[] args) {
     
		System.out.println("主线程执行");
		try {
     
			Thread.sleep(5000);
		} catch (Exception e) {
     
			e.getStackTrace();
		}
		
		System.out.println("线程休眠5秒后输出了");
	}

在这里插入图片描述

5秒后:

在这里插入图片描述

面试题:判断下面代码运行效果
public static void main(String[] args) {
     
		
		Thread t=new MyThread2();
		t.setName("t");
		t.start();
		
		try {
     
			
			//t线程会休眠吗??
			t.sleep(5000);
		} catch (Exception e) {
     
			e.printStackTrace();
		}
		
		System.out.println("主线程");
	}

}
class MyThread2 extends Thread{
     
	
	public void run() {
     
		for (int i = 0; i < 10; i++) {
     
			System.out.println("当前线程"+Thread.currentThread().getName()+"\t"+i);
		}
	}
}

分析:t.sleep()出现在主线程中,虽然它是由线程对象点出来的,但是它是静态方法和引用没有任何关系,出现在哪个地方,就会让哪个线程进入休眠,所有说这个程序会先执行分线程,5秒后输出”主线程“。

3.5.2 线程interrupt(“叫醒线程方法”)

interrupt方法叫”干扰“,会中断线程的睡眠,靠的是java的异常处理机制。

public static void main(String[] args) {
     
		Thread t=new MyThread2();
		//分支线程开启
		t.start();
		try {
     
			Thread.sleep(2000);//主线程休息2秒
		} catch (Exception e) {
     
			e.printStackTrace();
		}
		
		
		t.interrupt();//让分支线程型“醒过来”
	}

}
class MyThread2 extends Thread{
     
	
	public void run() {
     
		try {
     
			Thread.sleep(20000000);//线程休眠很长时间
		} catch (Exception e) {
     
			e.printStackTrace();
		}
		System.out.println("分支线程");
	}
}

线程_第4张图片
理解:当开始运行run方法的时候,线程会进入长时间休眠状态,通过执行interrupt方法会让分支线程出异常,直接执行catch语句。

3.5.3 线程stop(强行中断线程)

public static void main(String[] args) {
     
		Thread t=new MyThread2();
		t.start();
		
		//主线程休息5秒钟直接后干死分支线程
		try {
     
			Thread.sleep(5000);
		} catch (Exception e) {
     
			e.printStackTrace();
		}
		
		
		t.stop();//强行中断线程
	}

}
class MyThread2 extends Thread{
     
	
	public void run() {
     
		
		//这个程序会运行10秒钟
		for (int j = 0; j <10; j++) {
     
			System.out.println(Thread.currentThread().getName()+j);
			try {
     
				Thread.sleep(1000);
			} catch (Exception e) {
     
				e.printStackTrace();
			}
		}
		
		
	}
}

线程_第5张图片
stop的缺点:容易丢失数据,线程没有保存的数据会丢失,所以过时了。

四、线程的调度

4.1 常见的调度模型

抢占式:哪个线程的优先级比较高,抢到的cpu时间片的概率就多一些。java采用的就是抢占式调度模型。
均分式:平均分配cpu时间片,每个线程占有的cpu时间片时间长度一样,平均分配

4.2 java中的线程调度方法

实例方法:

void setpriority(int newPriority)//设置线程优先级
int getPriority()//获取线程优先级

静态方法:

static void yield()//暂停当前正在执行的线程,并执行其他线程

yield方法不是阻塞,会让当前线程”运行状态“回到”就绪状态“。
最低优先级是1,默认是5,最高是10.

五、多线程安全(重点)

5.1 什么时候会出现多线程并发的安全问题

需要满足三个条件

1.多线程并发环境下。
2.有共享数据。
3.共享的数据有修改行为。

5.2 解决方法(线程同步机制)

线程排队执行(不能并发),用排队执行解决线程安全问题,会牺牲一部分效率。
这种机制被成为:线程同步机制。

异步编程模型:线程t1和t2,各自执行,就做异步编程模型。就是多线程并发。

同步线程模型:线程t1执行的时候,必须等待线程2执行结束,两个线程之间出现了等待关系,这就是同步线程模型。

怎么实现同步机制呢,synchronized代码块

每一个堆中的对象都有一把锁,这把锁只是一个标记。
假设t1和t2线程并发,线程1先执行了,遇到了synchronized,这个时候自动找后面线程共享对象的对象锁,找到之后并占有一把锁,然后执行
同步代码块中的程序,在执行过程中会、一直占有这把锁,直到同步代码块结束,这把锁才会释放。
总而言之,一定时间只有一个线程会执行程序,下个线程在同步代码块外等待。

注意事项:需要同步线程一定要有个共享对象。

5.3 变量的线程安全问题

在java中有三大变量:
实例变量:在堆中
静态变量:在方法区
局部变量:在栈中

在这三者中只有局部变量永远不会有线程安全问题。原因在于局部变量在栈中,永远都不会共享。

解决方法:

1.使用局部变量代替实例和静态变量
2.如果用实例变量,多new对象。
3.迫不得已用synchronized,用线程同步机制。

5.4 死锁

就是多个线程在运行状态中因争夺资源造成的一种僵局。
线程_第6张图片

六、 守护线程

6.1 守护线程的实现

在Java中,有两大类线程:

第一种是用户线程,第二种是守护线程(也称作后台线程)。
守护线程具有代表是java中的垃圾回收线程。
主线程是一个用户线程。

守护线程的特点:

1.一般守护线程都是死循环。
2.用户线程结束,守护线程自动自动结束。
public static void main(String[] args) throws InterruptedException {
     
		
		Thread t1=new Thread(new Runnable() {
     
			
			
			//死循环线程
			public void run() {
     
				int i=0;
				while(true) {
     
					try {
     
						Thread.sleep(1000);
						System.out.println(Thread.currentThread().getName()+(i++));
					} catch (InterruptedException e) {
     
						
						e.printStackTrace();
					}
					
				}
			}
		});
		
		t1.setName("分支线程");
		
		t1.setDaemon(true);
		
		t1.start();
		
		//主线程执行代码
		
		for (int i = 0; i < 10; i++) {
     

			System.out.println(Thread.currentThread().getName()+i);
			Thread.sleep(1000);
		}
		

	}

}

执行结果:在主线程输出完之后,守护线程死循环也跟着结束了。

6.2 定时器

定时器有什么用呢?

目的是控制程序根据你设定的时间间隔去执行程序。
public static void main(String[] args) throws ParseException {
     
		Timer timer=new Timer();
		
		//日期类
		SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		
		//获取当前时间
		Date firstTime= sdf.parse("2021-4-22 15:10:00");
		
		
		timer.schedule(new LogTimerTask(),firstTime,1000);

	}

}

//定时任务类

class LogTimerTask extends TimerTask{
     

	
	public void run() {
     
		
		SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		
		String time=sdf.format(new Date());
		
		System.out.println(time+"完成任务!");
	}
	
}

线程_第7张图片

小结:计时器可采用线程守护的方式,可以用上方法,还能用匿名内部类形式。

七、notify和wait方法

7.1 wait方法

Object o=new Object();
o.wait();

wait方法的作用是,让o对象中正在运行的线程进入等待状态并释放掉o对象的锁,无限期等待,直到线程被再次唤醒。

7.2 notify方法

Object o=new Object();
o.notify();

notify的方法的作用是:让o对象进入等待状态的线程“苏醒”过来继续执行。
还有一个,notifyAll方法是让所有o对象的线程”苏醒“过来。

notify和wait方法都是在synchronized基础之上进行的。

7.3 两种方法的使用

交替打印奇数偶数

public static void main(String[] args) {
     
		Num num=new Num();
		Thread t1=new Thread(new Os(num));
		Thread t2=new Thread(new Js(num));
		t1.start();
		t2.start();

	}

}

class Num{
     
	int num=1;

	public int getNum() {
     
		return num;
	}

	public int printNum() {
     
		return num++;
	}
	
}

class Os implements Runnable{
     
	Num num;
	public Os(Num num) {
     
		this.num=num;
	}
	
	public void run() {
     
		while(num.getNum()<100) {
     
			synchronized (num) {
     
				if(num.getNum()%2==0) {
     
					
					try {
     
						num.wait();
					} catch (InterruptedException e) {
     
					
						e.printStackTrace();
					}
				System.out.println(Thread.currentThread().getName()+"\t"+num.printNum());
				num.notify();	
			}
		}
	}
}

class Js implements Runnable{
     
	Num num;
	
	public Js(Num num) {
     
		this.num=num;
	}
	
	public void run() {
     
		while(num.getNum()<100) {
     
			synchronized (num) {
     
				if(num.getNum()%2!=0) {
     
					try {
     
						num.wait();
					} catch (InterruptedException e) {
     	
						e.printStackTrace();
					}
				}
				System.out.println(Thread.currentThread().getName()+"\t"+num.printNum());
				num.notify();
				
			}
			
		}
		
	}
}

小结:01线程输出奇数,02线程输出.当数字1进入到Thread-0线程,synchrozed会锁住Num对象,进行判断为false,输出1,唤醒等待的thread-1线程执行。

你可能感兴趣的:(java,多线程)