JAVA基础学习- 多线程

1、线程的基本概念

程序:是一段可执行的静态代码

进程:程序的一次动态加载过程(将程序加载到内存时,此时程序就转换为了进程)

线程: 进程可进一步细化为线程,是一个程序内部的一条执行路径,线程不能独立存在,必须依附于某个进程

一个Java应用程序,至少有三个线程:

main线程,垃圾回收线程,异常处理线程

多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈

2、使用多线程的优点

优点在于充分利用了CPU的空闲时间片,用尽可能少的时间来对用户的要求做出响应,使得进程的整体运行效率得到较大提高,同时增强了应用程序的灵活性。

如果是单线程,那同时只能处理一个用户请求,多线程则可以同时处理多个用户的请求

3、上下文切换

上下文切换是指:一个工作的线程被另外一个线程暂停,另外一个线程占用处理器开始执行任务

扫描二维码关注公众号,回复: 14805957 查看本文章

CPU通过给每个线程分配CPU时间片来实现这个机制。时间片是CPU分配给各个线程的时间,因为时间片非常短,所以CPU通过不停地切换线程执行,让我们感觉多个线程是同时执行的。

4、多线程的创建方式

1.继承Thread类

实现步骤:

  • 创建一个继承Thread类的子类
  • 重写Thread类的run() 将线程需要执行的操作声明在run()
  • 创建Thread类子类的对象
  • 通过此对象调用start()

start()作用:启动当前线程 调用当前线程的run

2.实现Runnable接口

实现步骤:

  • 创建一个实现了Runnable接口的类
  • 实现类去实现Runnable中的抽象方法:run()
  • 创建实现类的对象
  • 将此对象作为参数传递到Thread类的构造器,创建Thread对象

//Thread类的构造器需要接收一个Runnable对象

public Thread(Runnable target)

通过Thread类的对象调用start()

以上两种方式的对比

  • 实现接口没有单继承的局限性
  • Thread类需要使用static来修饰共享数据,而实现Runnable接口就是将实现类当作参数传递给Thread构造器,run中数据天然就是共享数据

3.实现Callable接口

与实现Runnable接口相比,实现Callable接口功能更强大 我们需要重写call方法,call方法可以抛出异常

实现步骤:

  • 创建一个实现Callable的实现类。
  • 实现call()方法,将此线程需要执行的操作声明在call()中。
  • 创建Callable接口实现类的对象。
  • 将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象。
  • 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()方法。

4.线程池

优点:线程池,其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作

使用线程池中线程对象的步骤

  • 创建线程池对象
  • 创建Runnable接口子类对象
  • 提交Runnable接口子类对象
  • 关闭线程池

线程池方法:

  • Executors:线程池创建工厂类
  • public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象
  • ExecutorService:线程池类
  • Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行
    public class ThreadPool implements Runnable {
    
    @Override
    
    public void run() {
    
    System.out.println("获取一个线程");
    
    }
    
    }
    
    public static void main(String[] args) {
    
    //创建线程池对象
    
    ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
    
    //创建Runnable实例对象
    
    ThreadPool t = new ThreadPool();
    
    //从线程池中获取线程对象,然后调用ThreadPool中的run()
    
    service.submit(t);
    
    //再获取个线程对象,调用run()
    
    service.submit(t);
    
    //注意:submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭。将使用完的线程又归还到了线程池中
    
    //关闭线程池
    
    service.shutdown();
    
    }

5、线程的生命周期

  • 新建 :new Thread对象此时线程就是新建状态
  • 就绪 :新建的线程执行start方法就是就绪状态
  • 运行 :就绪的线程获取cpu执行权就是运行状态
  • 阻塞 :运行的线程遇到sleep wait 等待同步锁等情况变为阻塞状态
  • 死亡 :运行的线程执行完run 或者遇到stop 异常就变成死亡状态

6、线程同步

首先引入锁的概念:当某个方法或者代码块使用锁时,那么在同一时刻至多仅有有一个线程在执行该段代码

1.同步代码块

1.1实现Runnable接口

synchronized(锁){//任何一个类的对象 都可以充当锁

//需要被同步的代码 操作共享数据的方法

}
private int ticket=100;

@Override

public void run() {

while (true) {

synchronized (this) {

if (ticket > 0) {

try {

Thread.sleep(100);

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println(Thread.currentThread().getName() + "票号为" + ticket);

ticket--;

} else {

break;

}

}

}

}

多个线程必须要共用一把锁

由于是使用同一个runnable对象实例化出来的Thread对象,此时用this当前对象充当锁

1.2继承Thread类

继承与实现接口的方法基本一致,只需要确保锁是同一个锁即可

锁设置为static或者用当前类来充当锁

2.同步方法

操作共享数据的方法可以设置为同步方法

public synchronized void show(){

//操作共享数据的代码

}

非静态的同步方法 同步监视器是this

静态的同步方法 同步监视器是当前类本身

3.新增方式:Lock锁

lock中的方法

locke()方法: 加锁

unlock()方法: 释放锁

private ReentrantLock lock=new ReentrantLock();

//上锁

lock.lock();

//释放锁

lock.unlock();

synchronized和lock不同之处:

synchronized机制在执行完同步代码之后,自动释放锁

lock需要手动启动锁,手动关闭锁

7、死锁问题

所谓死锁是指多个进程因竞争资源而相互等待,若无外力作用,这些进程都无法向前推进

死锁产生的四个必要条件

  • 互斥条件:资源一次只允许一个进程访问
  • 不剥夺条件:已被占用的资源只能由属主释放,不允许被其它进程剥夺
  • 请求和保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
  • 循环等待条件:存在一个进程链,使得每个进程都占有下一个进程所需的至少一种资源

如何解决死锁问题:

破坏死锁产生的四个必要条件中的一个或多个

  • 破坏互斥:允许多个进程同时访问资源
  • 破坏不剥夺:必须释放以保持的资源
  • 破坏请求和保持:一次性分配所有资源
  • 破坏循环等待:定义资源类型的线性顺序来预防

8、线程通信

  • wait():一旦执行此方法,当前线程进入阻塞状态,并释放锁
  • notify():一旦执行此方法,就会唤醒一个wait的线程, wait方法释放锁,notify方法不释放锁
  • notifyAll(): 会唤醒所有被wait的线程
  • wait(),notify(),notifyAll()只能在同步代码块或者同步方法中执行

以上三个方法都是Object中的方法

sleep和wait方法的不同之处:

(1)方法声明的位置不同,sleep是Thread类中的方法,wait是Object类中的方法

(2)wait只能使用在同步代码块或者同步方法中

(3)wait会释放锁,而sleep不会释放锁

9、思考与提问

1.Thread类中的start()和run()方法有什么区别?

start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。

2.产生死锁的条件

1.互斥条件:一个资源每次只能被一个进程使用。 2.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。 3.不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。 4.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

3.你有哪些多线程开发良好的实践?

  • 给线程命名
  • 最小化同步范围
  • 优先使用volatile
  • 尽可能使用更高层次的并发工具而非wait和notify()来实现线程通信,如BlockingQueue,Semeaphore
  • 优先使用并发容器而非同步容器.
  • 考虑使用线程池

4.创建高铁卖票,总票数为100张, 如何把票同步且安全的卖出去?

Java 解决方案:同步机制

同步机制总结:对于并发工作,你需要某种方式来防止两个任务访问相同的资源(其实就是共享资源竞争)。防止这种冲突的方法就是当资源被一个任务使用时,在其上加锁。第一个访问某项资源的任务必须锁定这项资源,使其他仼务在其被解锁之前,就无法访问它了,而在其被解锁之时,另一个任务就可以锁定并使用它了。

5.前端的如何实现多线程?

Web worker(多线程编程)。JavaScript程序运行在主线程之外的另外一个线程中。将一些任务分配给后者运行。在主线程运行的同时,Worker(子)线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。应用场景:例如处理ajax返回的大批量数据,读取用户上传文件,计算MD5,更改canvas的位图的过滤,分析视频和声频文件等。

猜你喜欢

转载自blog.csdn.net/qq_25687271/article/details/124683945
今日推荐