当前位置:主页 > 查看内容

线程

发布时间:2021-09-17 00:00| 位朋友查看

简介:多线程 一、进程、线程 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……

一、进程、线程

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("主线程执行结束了!");

运行结果:
主线程执行结束了!
子线程执行结束了!
在这里插入图片描述

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

有没有函数可以让主线程等子线程结束了才结束运行主线程
有的。
通过在主线程的任务中用子线程调用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方法差不多。
在这里插入图片描述
下面代码有什么区别?

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.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("分支线程");
	}
}

在这里插入图片描述
理解:当开始运行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();
			}
		}
		
		
	}
}

在这里插入图片描述
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.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+"完成任务!");
	}
	
}

在这里插入图片描述

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

七、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线程执行。

;原文链接:https://blog.csdn.net/qq_52612109/article/details/115697882
本站部分内容转载于网络,版权归原作者所有,转载之目的在于传播更多优秀技术内容,如有侵权请联系QQ/微信:153890879删除,谢谢!
上一篇:暴力破解原理及实验 下一篇:没有了

推荐图文


随机推荐