多线程

打印 上一主题 下一主题

主题 910|帖子 910|积分 2730

1.相关概念

  • 步伐(program):为完成特定任务,用某种语言编写的一组指令的聚集。即指一段静态的代码,静态对象
  • 进程(process):步伐的一次执行过程,或是正在内存中运行的应用步伐。如:运行中的QQ,运行中的网易音乐播放器。
  • 线程(thread):进程可进一步细化为线程,是步伐内部的一条执行路径。一个进程中至少有一个线程。

    • 进程同一时间若并行执行多个线程,就是支持多线程的。

2.创建和启动线程
2.1 方式1:继承Thread类
Java通过继承Thread类来创建并启动多线程的步骤如下:

  • 定义Thread  类的子类,并重写该类的 run() 方法,该 run() 方法的方法体就代表了线程必要完成的任务
  • 创建 Thread 子类的实例,即创建了线程对象
  • 调用线程对象的 start() 方法来启动该线程
  1. 代码如下:
  2. package Thread;
  3. public class ThreadByThread extends Thread {
  4.     @Override
  5.     public void run() {
  6.         for (int i = 1; i <= 100; i++) {
  7.             if (i % 2 == 0) {
  8.                 System.out.println(Thread.currentThread().getName() + ":" + i);
  9.             }
  10.         }
  11.     }
  12. }
  13. 测试类:
  14. package Thread;
  15. public class ThreadTestByThread {
  16.     public static void main(String[] args) {
  17.         ThreadByThread t1 = new ThreadByThread();
  18.         ThreadByThread t2 = new ThreadByThread();
  19.         t1.start();
  20.         t2.start();
  21.     }
  22. }
复制代码
结果:发现卖出近100张票。

 问题:但是有重复票或负数票问题。

 原因:线程安全问题



解决方法:同步机制

 要解决上述多线程并发访问一个资源的安全性问题:也就是解决重复票与不存在

票问题,Java中提供了同步机制 (synchronized)来解决。





根据案例简述:
 窗口1线程进入操作的时候,窗口2和窗口3线程只能在外等着,窗口1操作结

束,窗口1和窗口2和窗口3才有机会进入代码去执行。也就是说在某个线程修改

共享资源的时候,其他线程不能修改该资源,等待修改完毕同步之后,才能去抢

夺CPU资源,完成对应的操作,保证了数据的同步性,解决了线程不安全的现

象。

为了保证每个线程都能正常执行原子操作,Java引入了线程同步机制。注意:在任

何时候,最多允许一个线程拥有同步锁,谁拿到锁就进入代码块,其他的线程只能

在外等着 (BLOCKED) 。


同步机制的原理,其实就相称于给某段代码加“锁”,任何线程想要执行这

段代码,都要先得到“锁”,我们称它为同步锁。因为Java对象在堆中的数

据分为分为对象头、实例变量、空白的填充。而对象头中包含:



  • Mark Word:记载了和当前对象有关的GC、锁标记等信息。
  • 指向类的指针:每一个对象必要记载它是由哪个类创建出来的。
  • 数组长度(只有数组对象才有)
哪个线程得到了“同步锁”对象之后,”同步锁“对象就会记载这个线程的

ID,这样其他线程就只能等待了,除非这个线程”开释“了锁对象,其他

线程才能重新得到/占"同步锁"对象。


同步代码块:synchronized 关键字可以用于某个区块前面,表示只对这个区块

的资源实行互斥访问。 格式:
  1. 代码如下:
  2. package Thread;
  3. public class ThreadByRunnable implements Runnable{
  4.     @Override
  5.     public void run() {
  6.         for (int i = 1; i <= 100; i++) {
  7.             if (i % 2 == 0) {
  8.                 System.out.println(Thread.currentThread().getName() + ":" + i);
  9.             }
  10.         }
  11.     }
  12. }
  13. 测试类:
  14. package Thread;
  15. public class ThreadTestByRunnable {
  16.     public static void main(String[] args) {
  17.         ThreadByRunnable r = new ThreadByRunnable();
  18.         Thread t1 = new Thread(r);
  19.         Thread t2 = new Thread(r);
  20.         t1.start();
  21.         t2.start();
  22.     }
  23. }
复制代码

同步方法:synchronized 关键字直接修饰方法,表示同一时刻只有一个线程能

进入这个方法,其他线程在外面等着。
  1. new Thread("新的线程!"){
  2.         @Override
  3.         public void run() {
  4.                 for (int i = 0; i < 10; i++) {
  5.                         System.out.println(getName()+":正在执行!"+i);
  6.                 }
  7.         }
  8. }.start();
复制代码

静态方法加锁代码:
  1. new Thread(new Runnable(){
  2.         @Override
  3.         public void run() {
  4.                 for (int i = 0; i < 10; i++) {
  5.                         System.out.println(Thread.currentThread().getName()+":" + i);
  6.                 }
  7.         }
  8. }).start();
复制代码

非静态方法加锁:
  1. package unsafe;
  2. class TicketSaleThread extends Thread {
  3.     private static int ticket = 100;
  4.     public void run() {
  5.         while (ticket > 0) {
  6.             try {
  7.                 Thread.sleep(10);//加入这个,使得问题暴露的更明显
  8.             } catch (InterruptedException e) {
  9.                 e.printStackTrace();
  10.             }
  11.             System.out.println(getName() + "卖出一张票,票号:" + ticket);
  12.             ticket--;
  13.         }
  14.     }
  15. }
  16. public class SaleTicket {
  17.     public static void main(String[] args) {
  18.         TicketSaleThread t1 = new TicketSaleThread();
  19.         TicketSaleThread t2 = new TicketSaleThread();
  20.         TicketSaleThread t3 = new TicketSaleThread();
  21.         t1.setName("窗口1");
  22.         t2.setName("窗口2");
  23.         t3.setName("窗口3");
  24.         t1.start();
  25.         t2.start();
  26.         t3.start();
  27.     }
  28. }
复制代码

6. 再谈同步


6.1 死锁
 差别的线程分别占用对方必要的同步资源不放弃,都在等待对方放弃自己必要

的同步资源,就形成了线程的死锁。





诱发死锁的原因:

  • 互斥条件
  • 占用且等待
  • 不可抢夺(或不可抢占)
  • 循环等待
以上4个条件,同时出现就会触发死锁。

解决死锁:

死锁一旦出现,基本很难人为干预,只能只管规避。可以思量打破上面的诱发条件。
针对条件1:互斥条件基本上无法被破坏。因为线程必要通过互斥解决安全问题。

针对条件2:可以思量一次性申请全部所需的资源,这样就不存在等待的问题。
针对条件3:占用部分资源的线程在进一步申请其他资源时,如果申请不到,就自动开释掉已经占用的资源。
针对条件4:可以将资源改为线性顺序。申请资源时,先申请序号较小的,这样避免循环等待问题。


6.2 JDK5.0 新特性:LOCK(锁)

  • JDK5.0的新增功能,保证线程的安全。与采用synchronized相比,Lock可

    提供多种锁方案,更灵活、更强大。Lock通过显式定义同步锁对象来实现同

    步。同步锁使用Lock对象充当。
  • java. util. concurrent. locks. Lock 接口是控制多个线程对共享资源进行访

    问的工具。锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对

    象加锁,线程开始访问共享资源之前应先得到Lock对象。
  • 实现线程安全的控制中,比较常用的是ReentrantLock,可以显式加锁、开释锁。

    • ReentrantLock类实现了 Lock 接口,它拥有与 synchronized 类似的

      并发性和内存语义,但是添加了类似锁投票、定时锁等候和可停止锁等

      候的一些特性。此外,它还提供了在激烈争用情况下更佳的性能。

  • Lock锁也称同步锁,加锁与开释锁方法,如下:

    • public void lock() :加同步锁。
    • public void unlock() :开释同步锁。



代码如下:
  1. package safe;
  2. class TicketSaleRunnable implements Runnable {
  3.     private int ticket = 100;
  4.     public void run() {
  5.         while (ticket > 0) {
  6.             try {
  7.                 Thread.sleep(10);//加入这个,使得问题暴露的更明显
  8.             } catch (InterruptedException e) {
  9.                 e.printStackTrace();
  10.             }
  11.             System.out.println(Thread.currentThread().getName() + "卖出一张票,票号:" + ticket);
  12.             ticket--;
  13.         }
  14.     }
  15. }
  16. public class SaleTicket {
  17.     public static void main(String[] args) {
  18.         TicketSaleRunnable tr = new TicketSaleRunnable();
  19.         Thread t1 = new Thread(tr, "窗口一");
  20.         Thread t2 = new Thread(tr, "窗口二");
  21.         Thread t3 = new Thread(tr, "窗口三");
  22.         t1.start();
  23.         t2.start();
  24.         t3.start();
  25.     }
  26. }
复制代码


synchronized 与 Lock 的对比

  • Lock 是显式锁(手动开启和关闭锁,别忘记关闭锁),synchronized是隐式锁,出了作用域、遇到异常等自动解锁
  • Lock 只有代码块锁,synchronized 有代码块锁和方法锁
  • 使用 Lock 锁,JVM 将花费较少的时间来调度线程,性能更好。并且具有更好的扩展性(提供更多的子类),更表现面向对象。
  • (了解)Lock 锁可以对读不加锁,对写加锁,synchronized 不可以
  • (了解)Lock 锁可以有多种获取锁的方式,可以从 sleep 的线程中抢到锁,synchronized 不可以

    说明:开辟建议中处理线程安全问题优先使用顺序为:

    Lock ----> 同步代码块 ----> 同步方法
7. 线程的通讯
生产者与消耗者问题
 等待唤醒机制可以解决经典的“生产者与消耗者”的问题。生产者与消耗者问

题,也称有限缓冲问题,是一个多线程同步问题的经典案例。该问题描述了两个

(多个)共享固定巨细缓冲区的线程——即所谓的“生产者”和“消耗者”——

在实际运行时会发生的问题。
 生产者的主要作用是天生肯定量的数据放到缓冲区中,然后重复此过程。与此
同时,消耗者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在
缓冲区满时参加数据,消耗者也不会在缓冲区中空时消耗数据。
举例:

 生产者(Productor)将产品交给伙计(Clerk),而消耗者(Customer)从伙计

处取走产品,伙计一次只能持有固定数量的产品(比如:20),如果生产者试

图生产更多的产品,伙计会叫生产者停一下,如果店中有空位放产品了再通

知生产者继续生产;如果店中没有产品了,伙计会告诉消耗者等一下,如果

店中有产品了再通知消耗者来取走产品。

类似的场景,比如厨师和服务员等。
生产者与消耗者问题中其实隐含了两个问题:

  • 线程安全问题:

    因为生产者与消耗者共享数据缓冲区,产生安全问题。不过这个问题可以使

    用同步解决。
  • 线程的和谐工作问题:

    要解决该问题,就必须让生产者线程在缓冲区满时等待(wait),暂停进入阻

    塞状态,等到下次消耗者消耗了缓冲区中的数据的时候,通知(notify)正在

    等待的线程恢复到就绪状态,重新开始往缓冲区添加数据。同样,也可以让

    消耗者线程在缓冲区空时进入等待(wait),暂停进入阻塞状态,等到生产者

    往缓冲区添加数据之后,再通知(notify)正在等待的线程恢复到就绪状态。

    通过这样的通讯机制来解决此类问题。

    代码实现:
  1. synchronized(同步锁){
  2.      需要同步操作的代码
  3. }
复制代码

8. JDK5.0 新增线程创建方式
8.1 新增方式一:实现 Callable接口

  • 与使用Runnable相比, Callable功能更强大些
–        相比run()方法,可以有返回值

–        方法可以抛出异常

–        支持泛型的返回值(必要借助FutureTask类,获取返回结果)


  • Future接口(了解)
–        可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。

–        FutureTask是Futrue接口的唯一的实现类

–        FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值


  • 缺点:在获取分线程执行结果的时候,当前线程(或是主线程)受阻塞,效率较低。
代码举例:
[code]/* * 创建多线程的方式三:实现Callable (jdk5.0新增的) *///1.创建一个实现Callable的实现类class NumThread implements Callable {    //2.实现call方法,将此线程必要执行的操作声明在call()中    @Override    public Object call() throws Exception {        int sum = 0;        for (int i = 1; i

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

商道如狼道

金牌会员
这个人很懒什么都没写!
快速回复 返回顶部 返回列表