java 多线程剖析

问题的缘由源自于一道简单的面试题:题目要求如下:

建立三个线程,A线程打印10次A,B线程打印10次B,C线程打印10次C,要求线程同时运行,交替打印10次ABC。

解决问题前我们前补充一些基本知识:

Runnable和Thread

线程的启动

线程的起动并不是简单的调用了你的RUN方法,而是由一个线程调度器来分别调用你的所有线程的RUN方法,
我们普通的RUN方法如果没有执行完是不会返回的,也就是会一直执行下去,这样RUN方法下面的方法就不可能会执行了,可是线程里的RUN方法却不一样,它只有一定的CPU时间,执行过后就给别的线程了,这样反复的把CPU的时间切来切去,因为切换的速度很快,所以我们就感觉是很多线程在同时运行一样.

你简单的调用run方法是没有这样效果的,所以你必须调用Thread类的start方法来启动你的线程.所以你启动线程有两种方法
一是写一个类继承自Thread类,然后重写里面的run方法,用start方法启动线程
二是写一个类实现Runnable接口,实现里面的run方法,用new Thread(Runnable target).start()方法来启动

这两种方法都必须实现RUN方法,这样线程起动的时候,线程管理器好去调用你的RUN方法.

start()和run()的关系

通过调用Thread类的start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。 然后通过此Thread类调用方法run()来完成其运行操作的, 这里方法run()称为线程体, 它包含了要执行的这个线程的内容, Run方法运行结束, 此线程终止, 而CPU再运行其它线程, 

而如果直接用Run方法, 这只是调用一个方法而已, 程序中依然只有主线程--这一个线程, 其程序执行路径还是只有一条, 这样就没有达到写线程的目的。

区别

Thread类是在java.lang包中定义的。一个类只要继承了Thread类同时覆写了本类中的run()方法就可以实现多线程操作了,但是一个类只能继承一个父类,这是此方法的局限。

实现Runnable接口相对于继承Thread类来说,有如下显著的好处: 

(1)适合多个相同程序代码的线程去处理同一资源的情况,把虚拟CPU(线程)同程序的代码,数据有效的分离,较好地体现了面向对象的设计思想。

(2)可以避免由于Java的单继承特性带来的局限。我们经常碰到这样一种情况,即当我们要将已经继承了某一个类的子类放入多线程中,由于一个类不能同时有两个父类,所以不能用继承Thread类的方式,那么,这个类就只能采用实现Runnable接口的方式了。

(3)有利于程序的健壮性,代码能够被多个线程共享,代码与数据是独立的。当多个线程的执行代码来自同一个类的实例时,即称它们共享相同的代码。多个线程操作相同的数据,与它们的代码无关。当共享访问相同的对象是,即它们共享相同的数据。当线程被构造时,需要的代码和数据通过一个对象作为构造函数实参传递进去,这个对象就是一个实现了Runnable接口的类的实例。

我在测试中发现,继承Thread的时候线程是顺序的,实现Runnable的时候确实不确定顺序的

wait和notify以及sleep

Obj.wait(),与Obj.notify()必须要与synchronized(Obj)一起使用,也就是wait,与notify是针对已经获取了Obj锁进行操作,从语法角度来说就是Obj.wait(),Obj.notify必须在synchronized(Obj){...}语句块内。从功能上来说wait就是说线程在获取对象锁后,主动释放对象锁,同时本线程休眠。直到有其它线程调用对象的notify()唤醒该线程,才能继续获取对象锁,并继续执行。相应的notify()就是对对象锁的唤醒操作。但有一点需要注意的是notify()调用后,并不是马上就释放对象锁的,而是在相应的synchronized(){}语句块执行结束,自动释放锁后,JVM会在wait()对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。这样就提供了在线程间同步、唤醒的操作。Thread.sleep()与Object.wait()二者都可以暂停当前线程,释放CPU控制权,主要的区别在于Object.wait()在释放CPU同时,释放了对象锁的控制。

对于sleep()方法,我们首先要知道该方法是属于Thread类中的。而wait()方法,则是属于Object类中的。

sleep()方法导致了程序暂停执行指定的时间,让出cpu该其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。

在调用sleep()方法的过程中,线程不会释放对象锁。

而当调用wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用notify()方法后本线程才进入对象锁定池准备

有了这下知识之后,下面我们来解决问题吧

题目剖析

从题目我们可以得到几个要点:

1:线程同时运行:但是对线程的启动没有做要求

2:每个线程打印十次对应的字母

3:交替打印:意味着三个线程的的打印操作有严格的顺序性

思路分析

1:每个线程打印不同的字母,操作类似,可以用统一的类来实现,并且用不用的自己初始化

2:线程之间之间有先后性,打印A的线程打印了,打印B的线程才能打印,然后到C,循环

如何保证线程之间的执行顺序的有序性是本题目的难点,

难点突破

打印的线程应该在打印一次A之后就应该进入等待状态,直到打印C的线程打印一次C之后在重新执行

打印B、C的线程也类似

简单思路

定义三个Object遍历a,b,c,每个线程同时获得这三个对象锁才打印对应的字母一次,打印一次后释放另外两个对象的锁

问题

每个线程都要同时获得三个对象锁才能打印,那个第一个线程打印完,如果只是释放另外两个对象的锁,那个就没有线程可以继续打印了;

如果三个都释放,那意味着当前线程可以继续打印了。

所以,用三个对象锁的思路似乎是行不通了

正确的思路之一(可能还有其他思路)

每个线程只有同时获得自己和自己的前缀对象的锁(例如A的前缀是C,B的是A),才能打印一次自己的字母,然后释放自己前缀对象的锁,进入自己前端对象的等待线程池里面,等待自己前缀对象被唤醒之后才能继续打印

那么自己前缀对象什么时候被唤醒呢?

很简单的思路,打印A的线程在打印一次A之后进入等待C对象的线程池里面,那么只要打印C的线程在打印自己的时候唤醒一下自己就好了,这样打印A的线程就可以在打印C的线程打印一次C之后马上可以打印一次A;

打印完一次自己后,马上唤醒自己,然后释放自己前缀对象的锁,进入自己前端对象的等待线程池里面,让自己的后缀线程可以马上打印,依次循环,问题好像已经解决了。

潜在问题

由于每个线程只拥有两个锁,并且只等待一个锁,那么对线程的启动顺序还是有要求的。假如启动的顺序是ACB,我们来分析一下:

线程A:一开始获得AC两个锁,打印A,唤醒A,进入等待C被唤醒的线程池,释放AC

线程B:一开始获得AB两个锁,打印B,唤醒B,进入等待A被唤醒的线程池,释放AB

线程C:一开始获得BC两个锁,打印C,唤醒C,进入等待B被唤醒的线程池,释放BC

这完全是没有问题的,所以我们必须保证打印ABC对应的线程顺序启动

做法很简单:启动A后短暂sleep一下就好,如果只是这样

        new Thread(aThread).start();
//        Thread.sleep(10);
        new Thread(bThread).start();
//        Thread.sleep(10);
        new Thread(cThread).start();
//        Thread.sleep(10);

如果你的类是implements Runnable的话,那个着三个线程启动的顺序是没有保证的,

当然,你可以改成extends Thread,这样的话这三个线程就是顺序启动的。

下面是完整的实现代码:

/**
 *
 * @author 戚伟杰
 * @version 2015年11月20日 上午10:15:07
 */
public class MyThreadPrint implements Runnable{
    private String name;
    private Object prev;
    private Object self;
    public MyThreadPrint(String name,Object prev,Object self) {
        // TODO Auto-generated constructor stub
        this.name = name;
        this.prev = prev;
        this.self = self;
    }
    @Override
    public void run() {
        // TODO Auto-generated method stub
        int count = 10;
        while(count>0){
            synchronized (prev) {
                synchronized (self) {
                    System.out.print(name);
                    count--;
//                    self.notify();
                }
                try {
                    prev.wait();
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    }
    public static void main(String[] args) throws InterruptedException{
        Object a = new Object();
        Object b = new Object();
        Object c = new Object();
        MyThreadPrint aThread = new MyThreadPrint("a",c,a);
        MyThreadPrint bThread = new MyThreadPrint("b",a,b);
        MyThreadPrint cThread = new MyThreadPrint("c",b,c);
        new Thread(aThread).start();
//        Thread.sleep(10);
        new Thread(bThread).start();
//        Thread.sleep(10);
        new Thread(cThread).start();
//        Thread.sleep(10);
//        aThread.run();
//        bThread.run();
//        cThread.run();
    }
}

p

时间: 2024-10-14 06:36:48

java 多线程剖析的相关文章

【52】java多线程剖析

线程的状态: 线程共有下面4种状态: 新建状态(New): 新创建了一个线程对象,当你用new创建一个线程时,该线程尚未运行. 就绪状态(Runnable): 线程对象创建后,其他线程调用了该对象的start()方法.该状态的线程位于可运行线程池中,变得可运行,等待获取CPU的使用权. 运行状态(Running): 就绪状态的线程获取了CPU,执行程序代码. 阻塞状态(Blocked): 阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行.直到线程进入就绪状态,才有机会转到运行状态. 阻塞

拨开云雾见天日 —— Java多线程编程概念剖析

说到Java多线程编程,大多数人都会想到继承Thread或实现Runnable编程,new 一个Thread实例,调用start()方法,由OS调用即可.具体过程如下: public class MyThread extends Thread {     @Override     public void run() {         System.out.println("MyThread");     }     public static void main(String[] 

synchronized与static synchronized 的差别、synchronized在JVM底层的实现原理及Java多线程锁理解

本Blog分为例如以下部分: 第一部分:synchronized与static synchronized 的差别 第二部分:JVM底层又是怎样实现synchronized的 第三部分:Java多线程锁,源码剖析 第一部分:synchronized与static synchronized的差别 1.synchronized与static synchronized 的差别 synchronized是对类的当前实例进行加锁,防止其它线程同一时候訪问该类的该实例的全部synchronized块.注意这里

synchronized与static synchronized 的区别、synchronized在JVM底层的实现原理及Java多线程锁理解

本Blog分为如下部分: 第一部分:synchronized与static synchronized 的区别 第二部分:JVM底层又是如何实现synchronized的 第三部分:Java多线程锁,源代码剖析 第一部分:synchronized与static synchronized的区别 1.synchronized与static synchronized 的区别 synchronized是对类的当前实例进行加锁,防止其他线程同时访问该类的该实例的所有synchronized块,注意这里是"类

java多线程系列1--线程实现与调度

java的重要功能之一就是内部支持多线程,这一系列文章将详细剖析java多线程的基础知识 多线程概述 多线程引入 程序只有一个执行流程,所以这样的程序就是单线程程序. 假如一个程序有多条执行流程,那么,该程序就是多线程程序. 进程:正在运行的程序,是系统进行资源分配和调用的独立单位.每一个进程都有它自己的内存空间和系统资源. 线程:是进程中的单个顺序控制流,是一条执行路径.一个进程如果只有一条执行路径,则称为单线程程序. 一个进程如果有多条执行路径,则称为多线程程序. Java程序运行原理 ja

java多线程以及Android多线程

Java 多线程 线程和进程的区别 线程和进程的本质:由CPU进行调度的并发式执行任务,多个任务被快速轮换执行,使得宏观上具有多个线程或者进程同时执行的效果. 进程:在操作系统来说,一个运行的程序或者说一个动态的指令集合通常对应一个进程Process,它是系统进行资源分配和调度的一个独立单位,也是拥有系统资源的基本单位.进程是系统中独立存在的实体,它可以拥有自己独立的资源,拥有自己私有的地址空间,进程之间不能直接访问其他进程的地址空间. 线程:线程是CPU调度的基本单位,也就是说在一个进程中可以

汪大神Java多线程编程实战

课程目录:├─1│  ├─Java并发编程.png│  ├─源码+ppt.rar│  ├─高并发编程第一阶段01讲.课程大纲及主要内容介绍.wmv│  ├─高并发编程第一阶段02讲.简单介绍什么是线程.wmv│  ├─高并发编程第一阶段03讲.创建并启动线程.mp4│  ├─高并发编程第一阶段04讲.线程生命周期以及start方法源码剖析.mp4│  ├─高并发编程第一阶段05讲.采用多线程方式模拟银行排队叫号.mp4│  ├─高并发编程第一阶段06讲.用Runnable接口将线程的逻辑执行单元

Java多线程视频教程并发编程面试知识

课程目录:  1-1.并发编程入门到实战课程简介1-2.什么是并发编程1-3.并发编程的挑战之频繁的上下文切换1-4.并发编程的挑战之死锁1-5.并发编程的挑战之线程安全1-6.并发编程的挑战之资源限制2-1.进程与线程的区别2-2.线程的状态及其相互转换2-3.创建线程的方式(上)2-4.创建线程的方式(下)2-5.线程的挂起及其恢复2-6.线程的中断操作2-7.线程的优先级2-8.守护线程3-1.什么是线程安全性3-2.从字节码角度剖析线程不安全操作3-3.原子性操作3-4.深入理解sync

Java多线程系列--“JUC锁”11之 Semaphore信号量的原理和示例

概要 本章,我们对JUC包中的信号量Semaphore进行学习.内容包括:Semaphore简介Semaphore数据结构Semaphore源码分析(基于JDK1.7.0_40)Semaphore示例 转载请注明出处:http://www.cnblogs.com/skywang12345/p/3534050.html Semaphore简介 Semaphore是一个计数信号量,它的本质是一个"共享锁". 信号量维护了一个信号量许可集.线程可以通过调用acquire()来获取信号量的许可