Thinking in Java 第十三章“并发”要点总结

1.进程与线程

进程:具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位.

线程:进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部在 使

用线程时,处理器将轮流给每个线程分配其占用时间。每个线程都觉得自己在一直占用 处理器,但事实上处理器时间是划分成片段分配给了所有的线程。

2.创建一个进程

写一个线程简单的做法是从java.lang.Thread继承,这个类已经具有了创建和运行线 程所必要的架构。Thread重要的方法是run( ),你得重载这个方法,以实现你要的功 能。这样,run()里的代码就能够与程序里的其它线程“同时”执行。

3.start()和run()方法

start() :

它的作用是启动一个新线程。
通过start()方法来启动的新线程,处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行相应线程的run()方法,这里方法run()称为线程体,它包含了要执行的这个线程的内容,run方法运行结束,此线程随即终止。start()不能被重复调用。用start方法来启动线程,真正实现了多线程运行,即无需等待某个线程的run方法体代码执行完毕就直接继续执行下面的代码。这里无需等待run方法执行完毕,即可继续执行下面的代码,即进行了线程切换。

run() :

run()就和普通的成员方法一样,可以被重复调用。
如果直接调用run方法,并不会启动新线程!程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到多线程的目的。

总结:调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程里执行。

4.让步 yield()

5.睡眠sleep()

是一个静态方法。

6.优先权 priority

优先级高的执行频率高,优先级低的执行频率低。

可以在setPriority(int priority)里面设置优先级 0 - 10

7.守护线程 daemon

所谓“守护”(daemon)线程,是指程序运行的时候,在后台提供一种通用服务的线程,并 且这种服务并不属于程序中不可或缺的部分。因此,当所有的非后台线程结束,程序也就 终止了。反过来说,只要有任何非后台线程还在运行,程序就不会终止。比如,执行main( ) 的就是一个非后台线程。

在Java中有两类线程:用户线程 (User Thread)、守护线程 (Daemon Thread)。 

所谓守护 线程,是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。反过来说,只要任何非守护线程还在运行,程序就不会终止。

用户线程和守护线程两者几乎没有区别,唯一的不同之处就在于虚拟机的离开:如果用户线程已经全部退出运行了,只剩下守护线程存在了,虚拟机也就退出了。 因为没有了被守护者,守护线程也就没有工作可做了,也就没有继续运行程序的必要了。

8.join()

ttps://www.cnblogs.com/lcplcpjava/p/6896904.html

https://blog.csdn.net/frankarmstrong/article/details/55504161

命名来源于posix标准。子线程join到主线程(启动程序的线程,比如c语言执行main函数的线程)。你的问题可能在于没有理解join,阻塞线程仅仅是一个表现,而非目的。其目的是等待当前线程执行完毕后,”计算单元”与主线程汇合。即主线程与子线程汇合之意。

main是主线程,在main中创建了thread线程,在main中调用了thread.join(),那么等thread结束后再执行main代码。

在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到join()方法了。比如

       

如果没有join方法,那么builder.toString方法就没有内容。

9.valotile关键字

在多线程环境下,线程可以将线程间共享的变量保存在本地内存(如寄存器)中,而不是从内存中读取,这就可能会引发不一致的问题,另一个进程可能在此线程运行期间改变了变量的值,而此线程并没有看到变化。

而volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。

10.interrupt方法

https://www.cnblogs.com/skywang12345/p/3479949.html

一看到线程的interrupt()方法,根据字面意思,很容易将该方法理解为中断线程。其实Thread.interrupt()并不会中断线程的运行,它的作用仅仅是为线程设定一个状态而已,即标明线程是中断状态,这样线程的调度机制或我们的代码逻辑就可以通过判断这个状态做一些处理,比如sleep()方法会抛出异常,或是我们根据isInterrupted()方法判断线程是否处于中断状态,然后做相关的逻辑处理。

11.Runnable接口

如果一个类需要继承一个基类,那么就不能继承Thread方法了,这时就必须实现Runnable接口,Thread类其实就是实现了Runnable接口。

Runnable类型的类只需一个run( )方法,但是如果你想要对这个Thread对象做点别 的事情(比如在toString( )里调用getName( )),那么你就必须通过调用 Thread.currentThread( )方法明确得到对此线程的引用。可以采用的Thread构造器 接受一个Runnable对象和一个线程名称作为其参数

12.TimeOut类

13.信号量

考虑简单的“信号量”(semaphore)概念,它可以看成是在两个线程之间进行通讯的标志 对象。如果信号量的值是零,则它监控的资源是可用的,但如果这个值是非零的,则被监控的资源不可用,所以线程必须等待。当资源可用的时候,线程增加信号量的值,然后继 续执行并使用这个被监控的资源。因为把增加和减少当作是原子操作(也就是不能被中断), 信号量能够保证两个线程同时访问同一资源的时候不至于冲突。

14.synchronized关键字

其行为很像一个信号量。

每个对象都含有一个单一的锁(也称为监视器),这个锁本身就是对象的一部分(你不用 写任何特殊代码)。有synchronized修饰的方法就会考虑对象锁,而没有synchronized修饰的方法就会无视对象锁。

一个线程可以多次获得对象的锁。如果一个方法在同一个对象上调用了第二个方法,后者 又调用了同一对象上的另一个方法,就会发生这种情况。JVM负责跟踪对象被加锁的次数。 如果一个对象被解锁,其计数为0。在线程第一次给对象加锁的时候,计数变为1。每次线 程在这个对象上获得了锁,计数都会增加。显然,只有首先获得了锁的线程才能允许继续 获取多个锁。每当线程离开一个synchronized方法,计数减少,当计数为零的时候,锁 被完全释放,此时别的线程就可以使用此资源。

15.原子操作

在有关Java线程的讨论中,一个常被提到的认识是“原子操作不需要进行同步控制”。“原 子操作”(atomic operation)即不能被线程调度机制中断的操作;一旦操作开始,那么它一定可以在可能发生的“上下文切换”(context switch)之前(切换到其它线程执 行)执行完毕。
还有一个常被提到的知识是,如果问题中的变量类型是除long或double以外的基本类型, 对这种变量进行简单的赋值或者返回值操作的时候,才算是原子操作。不包括long和 double的原因是因为它们比其它基本类型要大,所以JVM不能把对它的读取或赋值当成是 单一原子操作(也许JVM能够这么做,但这并不能保证)。然而,你只要给long或double 加上volatile,操作就是原子的了。

再java中甚至整型的自增操作也不是原子操作,因为这包括一个读写过程,这为线程出问题提供了空间。

16.临界区、同步控制块(synchronized)

采用同步控制块进行同步,对象不加锁的时间更长。这也是宁愿使用同步控制块而不是对整 个方法进行同步控制的典型原因:使得其它线程能更多地访问(在安全的情况下尽可能多)。

http://www.cnblogs.com/dolphin0520/p/3923737.html

有2种使用方法:

1.synchronized方法(对象锁):该对象的所有synchronized方法某一时刻只能运行一个。

2.synchronized块(比方法要灵敏、精细)(在synchronized后面的括号里面指定锁对象)

这样隐式的取得对象锁也行,谁先抢到谁先执行,没抢到就只有等

17.线程的状态

1.新建(new):线程对象已经建立,但还没有启动,所以它还不能运行。 2.就绪(Runnable):在这种状态下,只要调度程序把时间片分配给线程,线 程就可以运行。也就是说,在任意时刻,线程可以运行也可以不运行。只要调度 程序能分配时间片给线程,它就可以运行;这不同于死亡和阻塞状态。 3.死亡(Dead):线程死亡的通常方式是从run( )方法返回。在Java 2 废弃 stop( )以前,你也可以调用它,但这很容易让你的程序进入不稳定状态。还有 一个destroy( )方法(这个方法从来没被实现过,也许以后也不会被实现,它 也属于被废止的)。在本章的后面你将学习一种与调用stop( )功能等价的方式。
 4.阻塞(Blocked):线程能够运行,但有某个条件阻止它的运行。当线程处于 阻塞状态时,调度机制将忽略线程,不会分配给线程任何处理器时间。直到线程 重新进入了就绪状态,它才有可能执行操作

18.阻塞状态

1.你通过调用sleep(milliseconds)使线程进入休眠状态,在这种情况下, 线程在指定的时间内不会运行。

2.你通过调用 wait( )使线程挂起。直到线程得到了 notify( )或 notifyAll( )消息,线程才会进入就绪状态。我们将在下一节验证这一点。

3.线程在等待某个输入/输出完成。

4.线程试图在某个对象上调用其同步控制方法,但是对象锁不可用。

19.线程之间的合作-------握手-------wait()和notify()

1.wait:   wait(int long):效果和sleep(int long)类似,但是在wait期间,对象锁是释放的。

                在时间到了、notify()方法、notifyAll()方法被调用时,苏醒。

                wait():

                只有在notify()方法和notifyAll()方法被调用时,才会苏醒。

wait( ), notify( ),以及notifyAll( )的一个比较特殊的方面是这些方法是基类 Object的一部分,而不是像Sleep( )那样属于Thread的一部分。尽管开始看起来有点 奇怪,仅仅针对线程的功能却作为通用基类的一部分而实现,不过这是有道理的,因为这 些功能要用到的锁也是所有对象的一部分。所以,你可以把wait( )放进任何同步控制方 法里,而不用考虑这个类是继承自Thread还是实现了Runnable接口。实际上,你只能在 同步控制方法或同步控制块里调用wait( ), notify( )和notifyAll( )(因为不用 操作锁,所以sleep( )可以在非同步控制方法里调用)。如果你在非同步控制方法里调用 这些方法,程序能通过编译,但运行的时候,你将得到IllegalMonitorStateException 异常,伴随着一些含糊的消息,比如“当前线程不是拥有者”。消息的意思是,调用 wait( ), notify( )和notifyAll( )的线程在调用这些方法前必须“拥有”(获取)对象的锁。

简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中,因为锁属于对象(每个对象都有一把锁)。

生产者-消费者例子

20.用PipeWriter和PipeReader及PipeInputStream和PipeOutputStream在进程中间传递数据

记得关闭就行

21.死锁 deadlock 贪吃蛇头咬尾

哲学家吃意大利面的例子

class ChopStick{
    private static int counter = 0;
    private int number = counter++;

    @Override
    public String toString() {
        return "ChopStick "+number;
    }
}
class Philosopher extends Thread{
    private static Random rand = new Random();
    private static int counter = 0;
    private int number = counter++;
    private ChopStick leftChopStick;
    private ChopStick rightChopStick;
    static int ponder = 0;
    public Philosopher(ChopStick left,ChopStick right){
        leftChopStick=left;
        rightChopStick=right;
        start();
    }
    public void think(){
        int pro = rand.nextInt(ponder);
        System.out.println(this + "thinking "+pro);
        if(ponder>0){
            try {
                sleep(pro+1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
    public void eat(){
        synchronized (leftChopStick){
            System.out.println(this+" has "+this.leftChopStick+" waiting for "+this.rightChopStick);
            synchronized (rightChopStick){
                System.out.println(this +" eating");
            }
        }
    }

    @Override
    public String toString() {
        return "Philosopher "+number;
    }

    @Override
    public void run() {
        while(true){
            think();
            eat();
        }
    }

    public static void main(String[] args) {
        Philosopher[] philosophers = new Philosopher[3];
        Philosopher.ponder = 10;
        ChopStick left = new ChopStick();
        ChopStick right = new ChopStick();
        ChopStick first = left;
        int i = 0;
        while(i < philosophers.length-1){
            philosophers[i++] = new Philosopher(left,right);
            left=right;
            right = new ChopStick();
        }
        philosophers[i] = new Philosopher(left,first);
    }
}

22.总结

明白什么时候应该使用并发,什么时候应该避免使用并发是非常关键的。使用它的原因主 要是:要处理很多任务,它们交织在一起,能够更有效地使用计算机(包括在多个处理器 上透明地分配任务的能力),能够更好地组织代码,或者更便于用户使用。资源均衡的经 典案例是在等待输入/输出时使用处理器。使用户方便的经典案例是在长时间的下载过程中 监视“停止”按钮是否被按下。 

多线程的主要缺陷有: 1.等待共享资源的时候性能降低。 2.需要处理线程的额外CPU耗费。 3.糟糕的程序设计导致不必要的复杂度。 4.有可能产生一些病态行为,如饿死﹑竞争﹑死锁和活锁。 5.不同平台导致的不一致性。比如,当我在编写书中的一些例子时,发现竞争条件 在某些机器上很快出现,但在别的机器上根本不出现。如果你在后一种机器上做开 发,那么当你发布程序的时候就要大吃一惊了

猜你喜欢

转载自blog.csdn.net/weixin_38967434/article/details/82629188