文章目录
前言
如果一次只完成一件事情,会很容易实现,但在现实生活中很多事情都是同时进行的,所以在Java中为了模拟这种常态,引入了线程机制。简单来说,当程序同时完成很多事情时,就是所谓的多线程程序。 随着计算机的配置越来越高,我们需要将进程进一步优化,细分为线程,充分提高图形化界面的多线程的开发。这就要求对线程的掌握很彻底。 那么话不多说,进入正题,今天小伙子将记录自己线程的学习。提示:以下是本篇文章正文内容,下面案例可供参考
1、线程简介
1.1、线程与进程
程序:是为完成特定任务,用某种语言编写的一组指令的集合,即指一段静态的代码,静态对象。
进程:是程序的一次执行过程,或是正在运行的一个程序,是一个动态的过程,有它自身的产生,存在和消亡的过程。-------生命周期
线程:进程可进一步细化为线程,是一个程序内部的一条执行路径
1.2、同步与异步
同步:就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。也就是必须一件一件事做,等前一件做完了才能做下一件事.就像早上起床后,先洗涮,然后才能吃饭,不能在洗涮没有完成时,就开始吃饭.
异步:概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。
1.3、并行与并发
并行:多个cpu实例或者多台机器同时执行一段处理逻辑,是真正的同时。比如:多个人同时做不同的事
并发:通过cpu调度算法,让用户看上去同时执行,实际上从cpu操作层面不是真正的同时。并发往往在场景中有公用的资源,那么针对这个公用的资源往往产生瓶颈,我们会用TPS或者QPS来反应这个系统的处理能力。比如:一个CPU(采用时间片)同时执行多个任务,比如秒杀平台,多个人做同件事
2、实现线程的两种方式
2.1、继承Thread类
思路:
1.创建一个继承于Thread类的子类 (通过ctrl+o(override)输入run查找run方法)
2.覆写(重写)Thread类的run()方法
3.创建Thread子类(线程类)的对象
4.通过此对象调用start()方法(启动之后会自动调用重写的run方法执行线程)
继承Thread类创建一个新的线程语法如下:
public class MyThread extends Thread {
}
start与run方法的区别:
start方法的作用:1.启动当前线程 2.调用当前线程的重写的run方法(在主线程中生成子线程,有两条线程)
调用start方法以后,一条路径代表一个线程,同时执行两线程时,因为时间片的轮换,所以执行过程随机分配,且一个线程对象只能调用一次start方法。
run方法的作用:在主线程中调用以后,直接在主线程一条线程中执行了该线程中run的方法。(调用线程中的run方法,只调用run方法,并不新开线程)
总结:我们不能通过run方法来新开一个线程,只能调用线程中重写的run方法(可以在线程中不断的调用run方法,但是不能开启子线程,即不能同时干几件事),start是开启线程,再调用方法(即默认开启一次线程,调用一次run方法,可以同时执行几件事)
2.2、实现Runnable接口
因为Java语言中不支持多继承,这时候就需要实现Runnable接口使其具有使用线程的功能。
思路步骤
1、建立Runnable对象
2、使用参数为Runnable对象的构造方法创建Tread实例
3、调用start()方法启动线程
实现Runnable接口的语法如下:
public class MyThread extends Thread implements Runnable {
}
比较创建线程的两种方式:
开发中,优先选择实现Runable接口的方式
原因
1:实现的方式没有类的单继承性的局限性
2:实现的方式更适合用来处理多个线程有共享数据的情况
联系:Thread也是实现自Runable,两种方式都需要重写run()方法,将线程要执行的 逻辑声明在run中
3、线程的生命周期
概述:
线程具有生命周期,其中包含5种状态: 出生状态、就绪状态、运行状态、暂停状态(包括休眠、等待和阻塞等)以及死亡状态。
出生状态就是线程被创建时处于的状态,在用户使用该线程实例调用start()方法之前,线程都处于出生状态;当用户调用start()方法后,线程处于就绪状态(又被称为可执行状态);当线程得到系统资源后就进入运行状态。
一旦进入运行状态,它就会在就绪和运行状态下转换,同时也有可能进入暂停或者死亡状态。当处于运行状态下的线程调用 sleep()方法,wait()方或者阻塞解除时,会进入暂停状态;当休眠结束、调用notify()方法、notifyAll()方法或者阻塞解除时,会进入就绪状态;当线程的run()方法执行完毕,或者线程发生错误、异常时,线程进入死亡状态。
3.1、操作线程的方法
3.1.1、线程的休眠
sleep()方法的使用:
try{
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
3.1.2、线程的加入
join()方法的使用:
当某个线程使用join()方法加入另外一个线程时,另一个线程会等待该线程执行完毕后再继续执行。
3.1.3、线程的中断
以往有的时候会使用stop()方法停止线程,但JDK早已废除了stop()方法,不建议使用stop()方法来 停止一个线程的运行。现在提倡在run()方法中使用无限循环的形式,然后使用一个布尔类型标记控制循环的停止。
例如,创建一个InterruptedTest类,该类实现了Runnable接口,并设置线程正确的停止方式,
public class InterrupedTest implements Runnable{
private boolean isContinue = false; //设置一个标记变量,默认值为false
@Override
public void run() {
while(true){
//...
if(isContinue){
break;
}
}
}
public void setContinue(){
this.isContinue = true;
}
}
4、线程的同步
4.1、线程安全
什么是线程安全问题呢?
线程安全问题是指,多个线程对同一个共享数据进行操作时,线程没来得及更新共享数据,从而导致另外线程没得到最新的数据,从而产生线程安全问题。
上述例子中:创建三个窗口卖票,总票数为100张票
1.卖票过程中,出现了重票(票被反复的卖出,ticket未被减少时就打印出了)错票。
2.问题出现的原因:当某个线程操作车票的过程中,尚未完成操作时,其他线程参与进 来,也来操作车票。(将此过程的代码看作一个区域,当有线程进去时,装锁,不让别的线程进去)
生动理解的例子:有一个厕所,有人进去了,但是没有上锁,于是别人不知道你进去了,别人也进去了对厕所也使用造成错误。
3.如何解决:当一个线程在操作ticket时,其他线程不能参与进来,直到此线程的生命周期结束
4.在java中,我们通过同步机制,来解决线程的安全问题。
4.2、线程同步机制
方式一: 同步代码块
使用关键字Synchronized同步监视器(锁)
Synchronized(object){
//需要被同步的代码
}
public static void main(String[] args) {
//线程不安全
//解决方案1:同步代码块
//Object o = new Object();
Runnable run = new Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
private int count = 10;
private Object o = new Object();
@Override
public void run() {
while (true){
synchronized(o){
if(count > 0){
System.out.println("正在准备卖票!");
count--;
System.out.println(Thread.currentThread().getName()+"出票成功,余票:"+count);
try{
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
break;
}
}
}
}
}
注意:
操作共享数据的代码(所有线程共享的数据的操作的代码)(视作卫生间区域(所有人共享的厕所)),即为需要共享的代码(同步代码块,在同步代码块中,相当于是一个单线程,效率低)
共享数据:多个线程共同操作的数据,比如公共厕所就类比共享数据
同步监视器(俗称:锁):任何一个的对象都可以充当锁。(但是为了可读性一般设置英文成lock)当锁住以后只能有一个线程能进去(要求:多个线程必须要共用同一把锁,比如火车上的厕所,同一个标志表示有人)
Runable天生共享锁,而Thread中需要用static对象或者this关键字或者当前类(window。class)来充当唯一锁
方式二: 同步方法
使用同步方法,对方法进行synchronized关键字修饰
将同步代码块提取出来成为一个方法,用synchronized关键字修饰此方法。
对于runnable接口实现多线程,只需要将同步方法用synchronized修饰
而对于继承自Thread方式,需要将同步方法用static和synchronized修饰,因为对象不唯一(锁不唯一)
public static void main(String[] args) {
//线程不安全
//解决方案2:同步方法
//Object o = new Object();
Runnable run = new demo2.Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
private int count = 10;
@Override
public void run() {
while (true){
boolean flag = sale();
if(!flag){
break;
}
}
}
public synchronized boolean sale(){
if(count > 0){
System.out.println("正在准备卖票!");
count--;
System.out.println(Thread.currentThread().getName()+"出票成功,余票:"+count);
try{
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return true;
}
return false;
}
}
总结:
1.同步方法仍然涉及到同步监视器,只是不需要我们显示的声明。
2.非静态的同步方法,同步监视器是this静态的同步方法,同步监视器是当前类本身。继承自Thread class
方式三: 显示锁 Lock类 ReentrantLock子类
public static void main(String[] args) {
//线程不安全
//解决方案3:显示锁 Lock类 ReentrantLock子类
//同步代码块与同步方法都是隐式锁
Runnable run = new demo2.Ticket();
new Thread(run).start();
new Thread(run).start();
new Thread(run).start();
}
static class Ticket implements Runnable{
private int count = 10;
private Lock l = new ReentrantLock();
@Override
public void run() {
while (true){
l.lock();
if(count > 0){
System.out.println("正在准备卖票!");
count--;
System.out.println(Thread.currentThread().getName()+"出票成功,余票:"+count);
try{
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else{
break;
}
l.unlock();
}
}
}
总结:Synchronized与lock的异同?
相同:二者都可以用来解决线程安全问题
不同:synchronized机制在执行完相应的代码逻辑以后,自动 的释放同步监视器
lock 需要手动 的启动同步(lock()),同时结束同步也需要手动的实现(unlock())(同时以为着lock的方式更为灵活)
优先使用顺序:
LOCK -->> 同步代码块 -->> 同步方法
遇到问题解决步骤:
判断线程是否有安全问题,以及如何解决:
1.先判断是否多线程
2.再判断是否有共享数据
3.是否并发的对共享数据进行操作
4.选择上述三种方法解决线程安全问题
5、线程的死锁问题
线程死锁的理解:僵持,谁都不放手,一双筷子,我一只你一只,都等对方放手(死锁,两者都进入阻塞,谁都吃不了饭,进行不了下面吃饭的操作)
出现死锁以后,不会出现提示,只是所有线程都处于阻塞状态,无法继续
public class demo {
public static void main(String[] args) {
Police p = new Police();
Culrite c = new Culrite();
new MyThread(p,c).start();
p.say(c);
}
static class MyThread extends Thread{
private Police p;
private Culrite c;
public MyThread(Police p, Culrite c){
this.p = p;
this.c = c;
}
@Override
public void run() {
c.say(p);
}
}
static class Culrite{
public synchronized void say(Police p){
System.out.println("罪犯:你放了我,我放人质");
p.react();
}
public synchronized void react(){
System.out.println("罪犯被放走了,人质也被放了");
}
}
static class Police{
public synchronized void say(Culrite c){
System.out.println("警察:你放了人质,我放你走");
c.react();
}
public synchronized void react(){
System.out.println("人质被救了,但罪犯逃走了");
}
}
代码运行如下(执行不下去了(苦笑)):
如何解决死锁问题:
1.减少同步共享变量
2.采用专门的算法,多个线程之间规定先后执行的顺序,规避死锁问题
3.减少锁的嵌套。
6、线程的通信
6.1、常用通信方法
通信方法 | 描述 |
---|---|
wait() | 一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器 |
notify | 一旦执行此方法,就会唤醒被wait的一个线程,如果有多个线程,就唤醒优先级高的线程 |
notifyAll | 一旦执行此方法,就会唤醒所有被wait()的线程 |
特别提醒:这三个方法均只能使用在同步代码块或者同步方法中。
sleep和wait的异同:
相同点 | 一旦执行方法以后,都会使得当前的进程进入阻塞状态 |
---|---|
不同点 | 1.两个方法声明的位置不同,Thread类中声明sleep,Object类中声明wait。 2.调用的要求不同,sleep可以在任何需要的场景下调用,wait必须使用在同步代码块或者同步方法中 3.关于是否释放同步监视器,如果两个方法都使用在同步代码块或同步方法中,sleep不会释放,wait会释放** |
经典例题:生产者/消费者问题:
生产者(Priductor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如20个),如果生产者视图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产:如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
这里可能出现两个问题:
生产者比消费者快的时候,消费者会漏掉一些数据没有收到。
消费者比生产者快时,消费者会去相同的数据。
//交替实现,保证线程安全,
// 经典例题:生产者(厨师)与消费者(服务员)问题
public class demo {
public static void main(String[] args) {
Food f = new Food();
new Cook(f).start();
new Waiter(f).start();
}
//厨师
static class Cook extends Thread{
private Food f;
public Cook(Food f) {
this.f = f;
}
@Override
public void run() {
for(int i=0; i<100;i++){
if(i%2==0){
f.setNameTaste("老干妈小米粥","香辣味");
}else{
f.setNameTaste("煎饼果子","甜辣味");
}
}
}
}
//服务生
static class Waiter extends Thread{
private Food f;
public Waiter(Food f) {
this.f = f;
}
@Override
public void run() {
for(int i=0; i<100;i++){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
f.get();
}
}
}
//食物
static class Food{
private String name;
private String taste;
private boolean flag = true;
public synchronized void setNameTaste(String name, String taste) {
if(flag){
this.name = name;
this.taste = taste;
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = false;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void get(){
if(!flag){
System.out.println("服务生端走的菜的名称是:"+name+",味道是:"+taste);
}
flag = true;
this.notifyAll();
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}