Java多线程

第一章 多线程技能

线程与进程的区别

  • 一个进程的启动是由多个线程所支持的;windows进程管理器当中的第一项都一个进程,每一个进程可以由多个线程组成;每个线程的任务是不同的,互不影响
  • 进程之间的数据不可以共享,而线程可以

关键句

  • 一个进程至少有一个线程在运行
  • 代码的执行结果与执行顺序和调用顺序无关(调用随机性)
  • star方法的执行顺序不代表线程的执行顺序
  • Thread类也实现了Runnable接口,所以Thread的构造方法当中也可以传入Threa对象
  • 数据和方法若不做设置,会出现线程不安全的情况,导致同一份数据被多个线程操作,通过在run方法前添加synchronized关键字,控制线程的执行队列,如果有其他线程在操作,那么就需要等待其他线程执行完之后当前线程才可以操作,run方法被称做“互斥区”或“临界区”
  • 虽然synchronized可以解决数据安全的问题,但如果多个线程都在排队想要操作数据,而存在一个线程在操作,一直没有结束,那么其他所有的线程都必须在排队
  • run与start的区别是:start返回当前线程的名称是被Thread-0线程调用的结果
  • isAlive()方法用于判断线程是否活动;true/false取决于线程是否结束
  • getName()方法用于获取当前线程的名称
  • sleep()方法用于休眠当前线程
  • getId()方法用于获取当前线程的id
  • interrupted()方法与isInterrupted()方法
  • suspend()方法暂停线程,resume()方法恢复线程,但suspend()方法会独占公共变量,将变量永远锁住,并且会导致数据不同步的情况
  • Thread.yield()方法会放弃当前cpu资源,让利给其他资源使用,这样会导致线程执行时间变长
  • Thread.getPriority()方法可以获取线程的优先级,对应的setPriority()可以设置线程的优先级
  • 线程的优先级具有以下特点
    • 继承性:两个线程如果是子父关系,那么priority相等
    • 规则性:优先级高的会大部分先执行完
    • 随机性:随机执行各线程,而非根据优先级高的来执行
    • 优先级高的线程执行速度要快于优先级低的
  • isDaemon()判断一个线程是否是daemon线程(守护线程);如果是守护线程,如果线程都已经结束了,此标志才会被置为false

第二章 对象及变量的并发访问

2.1

  • 方法内部的变更不存在非线程安全问题
  • synchronized只会锁对象,不会锁方法;即创建多个对象,会产生多个锁;若只创建一个对象,则线程会顺序执行
  • 线程A调用obejct对象当中的synchronized方法时,B线程可以调用其他没有被同步的方法;如果B线程调用了其他也被synchronized声明的方法时,也需要排队等待,需要同步执行
  • 什么是脏读:因非线程安全取到的被修改过的数据;对于会产生脏读的方法需要添加synchronized
  • 什么叫锁重入:当一个synchronized声明的方法内部调用了其他被声明synchronized方法;
  • 一个线程遇到异常,它所持有的锁会全部释放
  • 同步不具有继承性;即子类的方法若没有被synchronized声明,而父类的方法被声明了,那么调用子类的时候不会排队

2.2

  • synchronized的弊端在于,一个方法若处理时间过长,其他线程需要排队很久;解决方法,将方法当中仅需要同步等待执行的代码声明为synchronized(同步代码块);这样其他线程就可以访问其他没有被同步的代码
  • synchronized(this)间具有同步性,如果在一个对象当中,那么代码块之间是同步的;即不同的线程调用同一对象的不同synchronized(this)声明代码块时会同步执行
  • 若synchronized(非this)对象,那么不周的线程操作不同的对象的代码块的时候不是同步,而是异步
  • 对于static的synchronized,有一个问题
package com.pgy.thread.sync;

/**
 * Created by admin on 10/05/2017.
 */
public class staticSyncTest {

    public static void main(String[] args) {
        ThreadA tha = new ThreadA();
        tha.setName("tha");
        tha.start();

        ThreadB thb = new ThreadB();
        thb.setName("thb");
        thb.start();

        ThreadC thc = new ThreadC(new Service());
        thc.setName("thc");
        thc.start();
    }
}

class Service {
    synchronized public static void printA() {
        System.out.println(Thread.currentThread().getName() + "进入printA()");
        try {
            Thread.sleep(3000);

        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + "离开printA()");
    }

    synchronized public static void printB() {
        System.out.println(Thread.currentThread().getName() + "进入printB()");
        System.out.println(Thread.currentThread().getName() + "离开printB()");
    }

    synchronized public void printC() {
        System.out.println(Thread.currentThread().getName() + "进入printC()");
        System.out.println(Thread.currentThread().getName() + "离开printC()");
    }

    /**
     * 为什么printC已经添加了synchronized,还是会异步执行
     * tha进入printA()
     * thc进入printC()
     * thc离开printC()
     * tha离开printA()
     * thb进入printB()
     * thb离开printB()
     */

}

class ThreadA extends Thread {

    @Override
    public void run() {
        super.run();
        Service.printA();
    }
}

class ThreadB extends Thread {

    @Override
    public void run() {
        super.run();
        Service.printB();
    }
}

class ThreadC extends Thread {
    private Service service;

    public ThreadC(Service service) {
        this.service = service;
    }

    @Override
    public void run() {
        super.run();
        service.printC();
    }
}
  • 同步的代码块不能对String作同步;因为String有常量池的缓存功能,即"A"==“A” 为true;这样会导致如果两个线程操作的String变更是同一值,则程序一直会被最先抢到资源的线程运行;而其他对象类型则不会;
  • 同步块可以用来解决不同程序无限等待的问题;即将可以不做同步的代码抽离出来进行处理;将需要的同步代码进行同步处理;或在对象内部new出不同的对象,对这些对象进行同步处理;
  • 若一个对象被多个线程所同步,即时属性改变,也还是同步执行

2.3 volatile

  • volatile的作用是让变量可以在多个线程当中可见
  • volatile不支持原子性,即变量会被修改,无法做到变量同步
  • 线程安全的两方面:原子性和可见性
  • volatile是让线程每次去公共内存当中取值,而不是私有的内存;所以线程拿到的变量值每次都是其他线程修改后最新的;公共内存与私有内存的区别?
  • AtomicInteger的作用:让线程同步操作变量
  • 用原子类进行操作的时候需要注意:一个非同步的方法里面调用原子类的同步方法的时候,这时候线程是不安全的,方法体里面还是需要声明为同步

volatile与synchronized的区别

  • volatile只能声明变量,synchronized可以声明方法和变量
  • volatile比synchronized更加轻量
  • volatile不会导致阻塞,因为只是声明的变量,变更可以在线程当中可见;随意修改

第三章 线程间通信

3.1 等待/通知机制

  • wait使线程停止运行,notify使停止的线程继续运行
  • wati和notify都需要在同步的代码块当中执行,否则会抛出InterruptedException
  • 每个锁的对象都有两个对列,一个是就绪队列,一个是唤醒队列;
  • notify不会立即释放资源,而是需要等到同步方法执行完成之后,才会释放
  • notify唤醒线程是随机的;多次执行notify或调用notifyAll()可以唤醒所有被等待的线程
  • 线程处理wait状态的时候,如果调用interrupt方法会抛出InterruptException异常,遇到异常,锁就会被被释放掉
  • wait(long)在一定时间内等待,超过时间自动唤醒
  • 通过操作一个类里面的set和get对某个变量进行操作;set和get可以用wait和notify来进行交替处理
  • 可以进行一生产多消费、一生产一消费、多生产多消费(操作栈)
  • pipedInputStream、pipedOutputStream、pipedReader、pipedWriter可以进行线程间的管道流操作;操作之前要connect
package com.pgy.thread.sync;

import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;

/**
 * 测试线程间通信--字节流
 * Created by admin on 12/05/2017.
 */
public class PipedStreamTest {

    public static void main(String[] args) throws IOException, InterruptedException {

        PipedDataTest pipedDataTest = new PipedDataTest();

        PipedInputStream pipedInputStream = new PipedInputStream();
        PipedOutputStream pipedOutputStream = new PipedOutputStream();

        pipedInputStream.connect(pipedOutputStream);

        PipedThreadRead pipedThreadRead = new PipedThreadRead(pipedInputStream, pipedDataTest);
        PipedThreadWrite pipedThreadWrite = new PipedThreadWrite(pipedOutputStream, pipedDataTest);

        pipedThreadWrite.start();
        pipedThreadRead.start();



    }
}

class PipedThreadRead extends Thread {
    private PipedInputStream pipedInputStream;
    private PipedDataTest    pipedDataTest;

    public PipedThreadRead(PipedInputStream pipedInputStream, PipedDataTest pipedDataTest) {
        this.pipedInputStream = pipedInputStream;
        this.pipedDataTest = pipedDataTest;
    }

    @Override
    public void run() {
        super.run();
        PipedDataTest.readData(pipedInputStream);
    }
}

class PipedThreadWrite extends Thread {
    private PipedOutputStream pipedOutputStream;
    private PipedDataTest     pipedDataTest;

    public PipedThreadWrite(PipedOutputStream pipedOutputStream, PipedDataTest pipedDataTest) {
        this.pipedOutputStream = pipedOutputStream;
        this.pipedDataTest = pipedDataTest;
    }

    @Override
    public void run() {
        super.run();
        PipedDataTest.writeData(pipedOutputStream);
    }
}

class PipedDataTest {

    public static void writeData(PipedOutputStream pipedOutputStream) {
        try {
            System.out.println("start write");
            for (int i = 0; i < 1000; i++) {
                String data = i + "";
                pipedOutputStream.write(data.getBytes());
            }

            System.out.println();
            pipedOutputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    public static void readData(PipedInputStream pipedInputStream) {
        try {
            System.out.println("start read");
            byte[] bytes = new byte[20];
            int readLen = pipedInputStream.read(bytes);

            while (readLen > 0) {
                String newData = new String(bytes, 0, readLen);
                System.out.print(newData);
                readLen = pipedInputStream.read(bytes);
            }
            System.out.println();

            pipedInputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

3.2 join方法的使用

  • join可以使当前线程阻塞,直到运行结束后才执行下面的逻辑代码
  • join的线程若被interrupt,则会打印出InterruptException异常
  • join(long)与sleep(long)的区别与相同
    • 两者都可以对线程造成一定时间的阻塞
    • 当时间到达后,join会释放锁,而sleep不会释放锁 ????

3.3 类ThreadLocal的使用

  • ThreadLocal当中提供每个线程自己绑定的值;static的变量在大家共享的值
  • ThreadLocal的存值和取值是使用了ThreadLocalMap
  • ThreadLocal当中的变量值具有隔离性
  • 重写ThreadLocal的initialValue()方法,可以解决ThreadLocal.get()返回Null的问题

3.4 InheritableThreadLocal

  • InheritableThreadLocal继续了ThreadLocal;目的是让子线程可以取到父线程当中的值

第四章 Lock的使用

4.1 Lock的使用

  • 使用ReentrantLock,可以在需要同步的代码前使用lock()方法,同步后使用unlock()方法,效果等同于synchronized
  • ReentrantLock.newCondition()方法可以创建出来一个Condition对象;此对象可以使用await()方法使线程进入WAITTING状态;注意,await()之前,必须使用ReentrantLock的lock()方法,否则会有异常
  • Condition当中的signal()方法相当于notify()方法;signalAll()方法相当于notifyAll()方法
  • ReentrantLock的boolean类型构造方法决定是公平锁(true)还是非公平锁(false)

4.2 使用ReentrantReadWriteLock类

  • ReentrantLock的方式效率低下,而ReentrantReadWriteLock可以加速代码的运行
  • 读写锁一个是读操作相关的锁,称为共享锁;另一个是写操作相关的锁称为排他锁;多个读锁之间不互斥;多个写锁之间是互斥的;这样可以保证读的时候锁是共享的,不会wating,加快代码的运行速度

第五章 定时器Timer

5.1 Timer的使用

  • Timer(true)构造方法将定时器设置为守护线程,运行结束后线程自己结束
  • 多个定时器在同时运行的时候,由于定时器是以队列的方式运行的;所以当前面的Timer耗时较长时,后面的任务运行时间就会被延迟
  • Timer的cancel()方法可以取消Timer下面的所有TimerTask;而TimerTask的cancel()只会取消当前的TimerTask;其他的还是正常运行
  • Timer的cancel()若没有取到锁,那么task信息不会被取消;而是正常执行;即Timer若不量static或者加锁的

第六章 单例模式与多线程

  • 单例模式分为懒汉(延时加载)和饿汉(立即加载);饿汉就是getInstance()方法里面取已经实例化的对象;懒汉是若对象没有被实例化,则new出来,而懒汉违背了单例的规则,因为多线程会New出来多个实例
  • 在延迟加载的懒汉模式上,将getInstance()方法添加synchronized关键字或添加同步代码块,可以解决多线程当中不是单例的问题,但是需要此种方式效率低下,会导致多线程阻塞
  • 使用DCL(double check locking)模式来解决懒汉加载时单例的问题
  • 可以将类声明为静态的,并且在类内部当中实例化,这样就可以保证线程是安全的
  • 静态内部类可以达到线程安全问题,但是如果遇到序列化对象时,默认运行的方式结果还是多例的,将getInstance()方法放置在readResolve()当中可以解决此问题
  • static代码块在使用类的时候已经实现了;所以可以将new Instance()的代码放到static当中,可以保证单例模式的安全性
public class StaticSingletonTest {
    public static StaticSingletonTest str = null;

    static {
        str = new StaticSingletonTest();
    }

    public static StaticSingletonTest getInstance() {
        return str;
    }

    public static void main(String[] args) {
        StaticSingeltonThread staticSingeltonThread = new StaticSingeltonThread();
        staticSingeltonThread.run();
    }

}

class StaticSingeltonThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(StaticSingletonTest.getInstance().hashCode());
        }
    }
}
  • 由于enum的构造方法也是静态的,所以可以在一个枚举类当中,实现单例模式,效果同上

第七章 其他总结

线程的状态

  • 线程的状态在Thread.state枚举当中;NEW、RUNNABLE、WAITING、BLOCKED、TIMED~WATING~、TERMINATED
  • NEW:线程被实例化后,未执行start方法
  • RUNNABLE:线程运行状态,当一个线程被实例化之后,在线程的内部的状态就是RUNNABLE(包括构造方法)
class StaticSingeltonThread extends Thread {

    public StaticSingeltonThread() {
        System.out.println("thread的构造方法:" + Thread.currentThread().getState());//RUNNABLE
    }

    @Override
    public void run() {
        System.out.println("thread的run方法" + Thread.currentThread().getState());//RUNNABLE

    }
}
  • WAITING:线程操作对象执行了wait()方法之后
  • TIMED~WAITING~:线程执行了sleep后的状态
  • BLOCKED:当线程在等待其他线程释放锁的状态
  • TERMINATED:线程运行结束后的状态

线程组

  • 线程组的作用是批量管理线程或线程组对象,有效地对线程或线程组对象进行组织
  • jvm当中的根线程组是system,再getParent()就会抛NPE异常

simpleDataFormat

  • SimpleDataFormat是线程不安全的,因为多个线程使用的simpleDateFormat操作的format格式是不一样的
  • 解决办法,将SimpleDateFormat对象的实例化方法放到ThreadLocal当中

线程异常处理

  • Thread.setDefaultUncaughtExceptionHandler()方法可以获取线程当中的异常信息
  • TrehadGroup.uncaughtException()方法可以获取线程组当中的异常

其他

Thread与Runnable的区别 - Thread与Runnable的区别在于Runnable的线程的资源可以共享,多个线程可以同时操作一个变量

线程的生命周期

  1. 创建:new出来一个Thread
  2. 就绪:加入到执行队列当中,等待获取cpu资源去执行
  3. 运行:获取到了cpu资源,然后去运行线程
  4. 阻塞:wait,join,sleep或者其他的操作让线程让出了cpu的资源
  5. 终止:线程运行结束或者调用stop方法

守护线程

jstack