Archive十二月 2017

[jdk8] jdk8函数式编程思考

零、导读

(一)文章中心

我用jdk8已经半年多了,基本操作已经入门,但是jdk8魅力并非集合流以及线程等等,它要传输的更加是编程方式的改变,要知道像javascript是可以传递函数的,而在以前的jdk版本中,虽然可以但是并非十分的方便。现在官方已经可以通过传递行为的方式传递给函数,让编程中可以对一些基本的动作更加一步的抽象,而不需要写更多的重复代码。

文章中会提到常用的几个函数式接口,这几个接口无疑带来了很多方便。像之前用jdk8的时候写的第一篇文章《[jdk8]Lambda、JDK8接口使用、自定义函数接口》的时候还懵懵懂懂这个过程,所以这篇算是响应第一篇的使用了。

阅读更多

[多线程]Java多线程05_Lock类使用

一. Lock介绍

jdk5中新增了Lock对象,可以用来更加精确的控制线程之间的安全以及线程之间的通讯。

二. ReentrantLock类的使用

通过生产者和消费者来示范ReentrantLock类的使用,这个例子中包含了Lock加锁解锁的使用以及线程之间的通讯

通过创建5个生产者和5个消费者,对同一个资源进行操作,一个生产者一次生产5个,一个消费者一次生产一个。

/**
 * 资源类,包含创建资源和消费资源的方法
 * @author liweidan
 * @date 2017.12.19 下午2:05
 * @email toweidan@126.com
 */
public class Library {
    /** 资源锁 */
    ReentrantLock lock = new ReentrantLock();
    /** 生产者通知 */
    Condition producerCond = lock.newCondition();
    /** 消费者通知 */
    Condition consumerCond = lock.newCondition();
    /** 资源 */
    List<String> stringList = new ArrayList<>();
    /**
     * 生产方法
     */
    public void produce() {
        while (true) {
            try {
                /** 开始加锁 */
                lock.lock();
                /** 如果为空才进入生产 */
                if (stringList.isEmpty()) {
                    for (int i = 0; i < 5; i++) {
                        stringList.add("String - " + i);
                    }
                    System.out.println(Thread.currentThread().getName() + ":生产完成");
                }
                /** 唤醒所有的消费者 */
                consumerCond.signal();
                /** 抢到线程的生产者开始睡眠 */
                producerCond.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
    /**
     * 消费方法
     */
    public void consume() {
        while (true) {
            try {
                /** 开始加锁 */
                lock.lock();
                /** 叛空,如果没有资源提醒消费者生产 */
                if (stringList.isEmpty()) {
                    /** 如果已经没有了则唤醒一个生产者 */
                    producerCond.signal();
                    /** 然后该线程进入休眠 */
                    consumerCond.await();
                } else {
                    /** 消费一个 */
                    stringList.remove(stringList.size() - 1);
                    System.out.println(Thread.currentThread().getName() + ":消费完成");
                    if (stringList.isEmpty()) {
                        /** 如果已经没有了则唤醒一个生产者 */
                        producerCond.signal();
                    } else {
                        /** 如果还有资源,唤醒其他消费者 */
                        consumerCond.signal();
                    }
                    /** 该线程进入休眠 */
                    consumerCond.await();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }
}

/**
 * 消费者演示Lock类
 * @author liweidan
 * @date 2017.12.19 下午2:05
 * @email toweidan@126.com
 */
public class Consumer implements Runnable {
    private Library library;
    public Consumer(Library library) {
        this.library = library;
    }
    @Override
    public void run() {
        library.consume();
    }
}

/**
 * 生产者演示Lock类
 * @author liweidan
 * @date 2017.12.19 下午2:04
 * @email toweidan@126.com
 */
public class Producer implements Runnable {
    private Library library;
    public Producer(Library library) {
        this.library = library;
    }
    @Override
    public void run() {
        library.produce();
    }
}

private static void startConsumerAndProducer() {
    Library library = new Library();
    Thread[] consumerThreads = new Thread[5];
    Thread[] producerThreads = new Thread[5];
    for (int i = 0; i < 5; i++) {
        consumerThreads[i] = new Thread(new Consumer(library), "Consumer" + i);
        producerThreads[i] = new Thread(new Producer(library), "Producer" + i);
    }
    for (int i = 0; i < 5; i++) {
        consumerThreads[i].start();
        producerThreads[i].start();
    }
}

// 结果:
Producer4:生产完成
Consumer1:消费完成
Consumer4:消费完成
Consumer0:消费完成
Consumer3:消费完成
Consumer2:消费完成
Producer2:生产完成
Consumer3:消费完成
Consumer2:消费完成
Consumer1:消费完成
Consumer4:消费完成
Consumer0:消费完成
Producer3:生产完成
......

可以看到,一个生产线程完成以后可以准确的有四个消费者消费,而且也不会像notifyAll()方法一样盲目的唤醒所有线程,控制粒度更加精细。

(一)公平锁和非公平锁

锁Lock分为公平锁以及非公平锁,两者的特点很好理解:

  1. 公平锁:公平所表示线程获取锁的顺序是按照线程加锁的顺序来分配的,即先来先得
  2. 非公平锁:谁先抢到锁就是谁的。

可以看到上面的例子,消费者基本都有按照一定的顺序来做业务,而非公平锁则只要在创建锁的使用使用ReentrantLock lock = new ReentrantLock(false);的构造方法来构建。

(二)Lock类部分方法的使用

  1. int getHoldCount() 查询当前线程锁定(调用lock()方法)的次数
  2. int getQueueLength() 查询当前正等待获取锁定线程的估计数
  3. int getWaitQueueLength(Condition condition) 获取等待一个条件锁的线程数(多少个线程调用了condition.await()方法)
  4. boolean hasQueueThread(Thread thread) 查询指定的线程是否正在等待获取此锁定
  5. booleanhasQueueThreads() 查询是否有线程正在等待此锁定
  6. boolean hasWaiters(Condition condition) 查询是否有现成正在等待与此锁有关的条件
  7. boolean isFair() 查询该锁是否是公平锁
  8. boolean isHeldByCurrentThread() 查询当前线程是否保持此锁定(是否调用了lock()方法)
  9. boolean isLocked() 查询此锁是否由任意线程锁定着(是否有线程调用了lock()方法)
  10. boolean lockInterruptibly()lock()方法作用一样,只不过是如果当前线程被中断了就会抛出异常
  11. boolean tryLock() 调用的时候如果没有其他线程持有锁则锁定,锁定成功返回true
  12. boolean tryLock(long timeout, TimeUnit unit) 指定在等待时间内尝试获取锁

(三)Condition类部分方法的使用

  1. awaitUninterruptibly() 调用此方法如果线程在wait状态则不被标记interrupted状态
  2. awaitUntil(Date deadLine) 线程在指定时间到达前可以被唤醒。

三. ReentrantReadWriteLock类的使用

这是一个把读写锁分离的锁

使用也很简单:
1. 通过ReentrantReadWriteLock lock = new ReentrantReadWriteLock()
2. 通过lock.readLock().lock()对读取操作进行加锁
3. 通过lock.writeLock().lock()对写入操作进行加锁

特性:
1. 读写、写写互斥
2. 读读不互斥

示例读读不互斥

/**
 * 
 * @author liweidan
 * @date 2017.12.19 下午5:12
 * @email toweidan@126.com
 */
public class Library {
    public List<String> stringList;
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    public void read() {
        /** 锁定读取锁 */
        lock.readLock().lock();
        /** 开始读取数据 */
        Iterator<String> iterator = stringList.iterator();
        while (iterator.hasNext()) {
            String next = iterator.next();
            System.out.println(Thread.currentThread().getName() + ": " + next);
        }
        /** 解锁 */
        lock.readLock().unlock();
    }
}

/**
 * 写进程
 * @author liweidan
 * @date 2017.12.19 下午5:09
 * @email toweidan@126.com
 */
public class ReadThread implements Runnable {
    private Library library;
    public ReadThread(Library library) {
        this.library = library;
    }
    @Override
    public void run() {
        library.read();
    }
}

/**
 * 
 * @author liweidan
 * @date 2017.12.19 下午5:10
 * @email toweidan@126.com
 */
public class ThreadStart {
    public static void main(String[] args) {
        startReadThreads();
    }
    private static void startReadThreads() {
        Library aLibrary = new Library();
        aLibrary.stringList = new ArrayList<>(5);
        for (int i = 0; i < 10; i++) {
            aLibrary.stringList.add("String" + i);
        }
        for (int i = 0; i < 5; i++) {
            new Thread(new ReadThread(aLibrary)).start();
        }
    }
}

// 结果: 
...
Thread-3: String8
Thread-3: String9
Thread-0: String5
Thread-0: String6
Thread-0: String7
Thread-0: String8
Thread-0: String9
Thread-1: String5
Thread-1: String6
Thread-1: String7
Thread-1: String8
Thread-1: String9
...
这是乱序打印的

但是如果加上去写的操作那么就是互斥的了:

/**
 * 
 * @author liweidan
 * @date 2017.12.19 下午5:12
 * @email toweidan@126.com
 */
public class Library {
    public List<String> stringList;
    ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

     ......

    public void write() {
        /** 锁定读取锁 */
        lock.writeLock().lock();
        /** 开始读取数据 */
        for (int i = 0; i < 10; i++) {
            stringList.add("StringO" + i);
        }
        /** 解锁 */
        lock.writeLock().unlock();
    }
}

/** 启动线程用于写数据以及读数据 */
private static void startReadThreads() {
    Library aLibrary = new Library();
    aLibrary.stringList = new ArrayList<>(5);
    for (int i = 0; i < 5; i++) {
        new Thread(new WriteThread(aLibrary)).start();
        new Thread(new ReadThread(aLibrary)).start();
    }
}

// 结果:
Thread-5: StringO8
Thread-5: StringO9
WriteThread: StringO0
WriteThread: StringO1
WriteThread: StringO2
WriteThread: StringO3
WriteThread: StringO4
WriteThread: StringO5
WriteThread: StringO6
WriteThread: StringO7
WriteThread: StringO8
WriteThread: StringO9
Thread-7: StringO0
Thread-7: StringO1
Thread-7: StringO2
Thread-7: StringO3
Thread-9: StringO0
Thread-9: StringO1

// 可以看到读库是乱序的,但是写库是一定在一起的

四. 总结

这一篇写起来发现特别简单,因为复用了之前的东西,只是用法不同了,所以并没有写多少。

  1. Lock类的使用,其中分为普通的锁以及读写锁
  2. 普通锁的使用以及公平锁和非公平锁的选择
  3. Condition决定唤醒的对象,不需要像以前那样全部唤醒
  4. 读写锁分开读和写两个操作,提高线程的性能。

[多线程]Java多线程04_多个线程协作以及线程私有数据

一、线程间的通讯

前面说完了线程的工作、线程的安全等等,那么多个线程的协同工作,线程之间的通讯就是接下来的话题了。

线程之间的等待通知,简而言之就是一个线程做完了自己该做的一件事情,然后通知另外一个线程继续需要的业务操作,实现一个任务由多个线程异步进行从而提升软件性能。

$seq
线程A->线程A: 执行任务
Note right of 线程B: 等待线程A执行完成通知
线程A->线程B: 执行完成,通知线程B启动
Note right of 线程B: 继续执行任务
$

(一)什么时候用到

线程之间的通知需要应用在有对象级别锁的时候,比如前几个说的synchronized方法或者代码块,从而使用锁来进行通知(这时候,Objectwait()notify()以及notifyAll()方法就派上用场了)。

在锁对象调用wait()的时候,它会释放当前占有的锁,线程进入阻塞状态,等待其他线程调用该对象所的notify方法才继续占领锁运行,如果没有调用notify方法则这些线程会一直阻塞下去。如果有多个线程进入等待,则由线程规划器挑选出阻塞状态的线程继续运行。

二、线程通讯实战

(一)简单例子

创建一个线程,执行的时候进入了等待状态,而主线程在3秒以后唤醒他让他继续执行。


/** * 一个进入等待的线程 * @author liweidan */ public class WaitThread implements Runnable { private Object aLock; public WaitThread(Object aLock) { this.aLock = aLock; } @Override public void run() { try { synchronized (aLock) { System.out.println("线程开始进入等待"); aLock.wait(); System.out.println("线程被唤醒,结束了运行"); } } catch (InterruptedException e) { e.printStackTrace(); } } } public class ThreadStart { public static void main(String[] args) { startWaitDemo(); } public static void startWaitDemo() { try { /** 创建一个公共锁 */ Object aLock = new Object(); /** 通过已有的锁创建线程 */ WaitThread waitThread = new WaitThread(aLock); /** 启动线程 */ new Thread(waitThread).start(); /** aLock需要在另外一个线程的监视器中执行 */ synchronized (aLock) { /** 主线程休息3秒 */ Thread.sleep(3000L); /** 唤醒阻塞的线程 */ aLock.notify(); } } catch (InterruptedException e) { e.printStackTrace(); } } } // 结果: 线程开始进入等待 线程被唤醒,结束了运行

注意:
1. aLock一定需要是监视器状态的时候,也就是在synchronized代码块中调用;
2. wait()调用以后,线程会立即释放锁。

(二)wait()立即释放锁而notify()不立即释放

  1. wait()执行完成以后,可以看到线程直接在执行处停了下来并且释放锁。
  2. notify()执行完以后只是通知正在阻塞的线程说可以继续执行下去,但是如果notify()后面还有其他业务代码,则本线程会把后面的业务代码执行完成才算是把锁释放,这时候其他阻塞线程才会开始执行。

1. Thread生命周期

虽然说用时序图来表示不太符合常规,但是因为其箭头的清晰可见,所以我还是用时序图来表示。

线程一共有四个状态:新的线程、运行的线程、暂停的线程以及销毁的线程

$seq
新的线程->运行的线程: start()
新的线程->销毁的线程: stop()
运行的线程->暂停的线程: suspend() / sleep() / wait()
暂停的线程->运行的线程: resume()
运行的线程->运行的线程: yield()
运行的线程->销毁的线程: stop()或者run()结束
$

(三)几个需要注意的地方

  1. 当一个线程正处于wait状态的时候,调用interrupt()方法会抛出InterruptedException
  2. 方法wait(long)作用是:进入wait多少秒以后没有其他线程唤醒则该线程自动唤醒
  3. 通知过早:线程监听器在调用wait()之前就已经由其他线程调用notify(),则该通知无效,线程将一直等待下去
  4. wait()执行的条件发生变化的时候,需要注意条件的设置,以免产生错乱。

(四)生产者与消费者

生产者与消费者模式是最经典的案例了,意思就是生产者生产完数据以后要通知消费者来消费,但是如果生产者或者消费者是多个的情况下,可能出现以下几种情况:

生产者或者消费者使用notify()唤醒的可能是同类(生产者唤醒生产者,消费者唤醒消费者),那么在判断资源的时候可能出现都是空,导致所有线程都在等待,这时候程序就进行不下去了。可以使用notifyAll()来解决。

因为有几种情况:一对一,一对多,多对一。这里就只示范多对多情况,因为解决了这个最复杂的情况,其他貌似都不会太难了。

/**
 * 生产者线程
 * @author liweidan
 * @date 2017.12.18 下午4:28
 * @email toweidan@126.com
 */
public class ProducerThread implements Runnable {
    /** 资源 */
    private List<String> library;
    public ProducerThread(List<String> library) {
        this.library = library;
    }
    @Override
    public void run() {
        synchronized (library) {
            while (true) {
                if (library.isEmpty()) {
                    for (int i = 0; i < 5; i++) {
                        System.out.println("生产者生产: " + "library" + i);
                        library.add("library" + i);
                    }
                    /** 生产完成,通知全部消费者以及生产者 */
                    System.out.println("完成生产,通知全部");
                    library.notifyAll();
                } else {
                    /** 如果资源不为空则进入等待 */
                    try {
                        System.out.println("资源不为空,生产者进入等待");
                        library.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

/**
 * 消费者线程
 * @author liweidan
 * @date 2017.12.18 下午4:28
 * @email toweidan@126.com
 */
public class ConsumerThread implements Runnable {
    /** 资源 */
    private List<String> library;
    public ConsumerThread(List<String> library) {
        this.library = library;
    }
    @Override
    public void run() {
        synchronized (library) {
            while (true) {
                if (!library.isEmpty()) {
                    /** 移除最后一个 */
                    System.out.println("消费者消费: " + library.get(library.size() - 1));
                    library.remove(library.size() - 1);
                    /** 消费完成,通知全部消费者以及生产者 */
                    library.notifyAll();
                } else {
                    /** 如果资源不为空则进入等待 */
                    System.out.println("消费者:资源为空进入等待");
                    try {
                        library.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

public static void startProdAndCous() {
    List<String> lib = new ArrayList<>();
    /** 5个生产者、5个消费者 */
    for (int i = 0; i < 5; i++) {
        new Thread(new ConsumerThread(lib)).start();
        new Thread(new ProducerThread(lib)).start();
    }
}

// 结果:
......
资源不为空,生产者进入等待
消费者消费: library4
消费者消费: library3
消费者消费: library2
消费者消费: library1
消费者消费: library0
消费者:资源为空进入等待
生产者生产: library0
生产者生产: library1
生产者生产: library2
生产者生产: library3
生产者生产: library4
完成生产,通知全部
资源不为空,生产者进入等待
资源不为空,生产者进入等待
消费者消费: library4
消费者消费: library3
消费者消费: library2
消费者消费: library1
消费者消费: library0
消费者:资源为空进入等待
消费者:资源为空进入等待
......

可以看到,结果已经十分明显,可以实现资源的生产与消费。

这里还有个问题,就是唤醒的时候没有分清楚唤醒谁,在接下来的Lock可以实现。

三、线程之间数据的数据通讯

jdk提供了两对类,分别用于传输数据流以及字节流,对应IO流来学习即可,分别是:
1)PipedInputStream和PipedOutputStream
2)PipedReader和PipedWriter

/**
 * 读的线程
 * @author liweidan
 * @date 2017.12.18 下午5:08
 * @email toweidan@126.com
 */
public class ReadThread implements Runnable {
    private PipedInputStream inputStream;
    public ReadThread(PipedInputStream inputStream) {
        this.inputStream = inputStream;
    }
    @Override
    public void run() {
        try {
            while (true) {
                byte[] arr = new byte[1024];
                int len = inputStream.read(arr);
                while (len != -1) {
                    System.out.println("收到数据,开始读取数据: ");
                    String str = new String(arr, 0, len);
                    System.out.println(str);
                    len = inputStream.read(arr);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

/**
 * 写的线程
 * @author liweidan
 * @date 2017.12.18 下午6:00
 * @email toweidan@126.com
 */
public class WriteThread implements Runnable {
    private PipedOutputStream outputStream;
    public WriteThread(PipedOutputStream outputStream) {
        this.outputStream = outputStream;
    }
    @Override
    public void run() {
        try {
            while (true) {
                Scanner scanner = new Scanner(System.in);
                String str = scanner.next();
                outputStream.write(str.getBytes());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

/**
 * 
 * @author liweidan
 * @date 2017.12.18 下午6:05
 * @email toweidan@126.com
 */
public class ThreadStart {
    public static void main(String[] args) {
        try {
            PipedOutputStream outputStream = new PipedOutputStream();
            PipedInputStream inputStream = new PipedInputStream();
            inputStream.connect(outputStream);
            new Thread(new ReadThread(inputStream)).start();
            new Thread(new WriteThread(outputStream)).start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

// 结果: 
dsasd
收到数据,开始读取数据: 
dsasd
HelloWorld
收到数据,开始读取数据: 
HelloWorld

两个重点:
1. 当inputStreamwhile里面已经”枯竭”的时候,就会继续等待,等有数据进来的时候,再把它打印出来。
2. 通过inputStream.connect(outputStream);这个方法连接两个流

PS:貌似开发中比较少用这个?

PipedReader和PipedWriter使用与字节流相似。

四、等待线程运行——join使用

作用:创建线程后,调用线程的join方法。那么调用的线程会停下来(或指定停下来多长时间),等待线程执行完成输出结果,调用线程才继续运行。

代码所示主线程创建了A线程后,等待A线程执行完成再继续执行主线程。

/**
 * Join使用
 * @author liweidan
 * @date 2017.12.19 上午9:54
 * @email toweidan@126.com
 */
public class JoinDemo implements Runnable {
    @Override
    public void run() {
        try {
            /** 随机休眠时间 */
            double v = Math.random() * 3;
            Thread.sleep((long) v);
            /** 输出结果 */
            System.out.println("wait in " + v + "ms");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

private static void startJoinDemo() {
    try {
        Thread aThread = new Thread(new JoinDemo());
        aThread.start();
        aThread.join();
        System.out.println("Main end in " + System.currentTimeMillis());
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

// 结果: 
wait in 1.9918097432208672ms
Main end in 1513648665010  // 可以看到Main end 总是在线程执行完成后的输出

当然也会有坑:当我们通过join指定等待时间的时候,有可能多个线程之间互相抢夺资源但是主线程抢到了发现时间已经足够了就提前给执行后面的逻辑。

在这个例子中,我把join线程修改为睡眠2秒,然后再启动其他线程,多个线程进行抢夺资源。

/**
 * Join使用
 * @author liweidan
 * @date 2017.12.19 上午9:54
 * @email toweidan@126.com
 */
public class JoinDemo implements Runnable {
    @Override
    public void run() {
        try {
            /** 随机休眠时间 */
            double v = Math.random() * 3;
            /** 指定休息2秒 */
            Thread.sleep(2000);
            /** 输出结果 */
            System.out.println("wait in " + v + "ms");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

private static void startJoinDemo02() {
    try {
        Thread aThread = new Thread(new JoinDemo());
        Thread otherThread = new Thread(() -> {
            try {
                Thread.sleep(2000L);
                System.out.println("other end");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        aThread.start();
        otherThread.start();
        aThread.join(1000L);
        System.out.println("Main end in " + System.currentTimeMillis());
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

// 结果: 
Main end in 1513649551229 // main提前结束了。
other end
wait in 0.8592433100474021ms

运行图解:

$seq
主线程->aThread: 创建A线程
主线程->otherThread: 创建其他线程
主线程->aThread: 启动A线程
主线程->otherThread: 启动其他线程
主线程->aThread: 等待1秒
Note right of aThread: aThread等待2秒
Note right of 主线程: 主线程发现已经停够1秒,打印
$

五、线程私有变量存储: ThreadLocal和InheritableThreadLocal

我们可以这么理解,每个线程执行的空间是相互隔离,当有需要在线程内部进行数据传递的时候可以通过ThreadLocal以及InheritableThreadLocal(可以获取父线程的值)来做数据,相当于一个线程之间的小数据库,用于存储相应的数据的。像我们项目就是在每次请求进来设置当前线程的值是用户类,然后在后面的各个分层如果需要拿到用户信息,即可快速获取。

举个例子:创建两个线程,分别调用同一个方法,这个方法从ThreadLocal获取值,证明每个线程拿到的值是当前线程设置的。

/**
 * 线程私有变量
 * @author liweidan
 * @date 2017.12.19 上午10:47
 * @email toweidan@126.com
 */
public class ThreadHolder {
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    public static String set(String value) {
        threadLocal.set(value);
        return value;
    }
    public static String get() {
        return threadLocal.get();
    }
}

/**
 * 用于给线程调用,证明不同线程输出来的值是不一样的
 * @author liweidan
 * @date 2017.12.19 上午10:52
 * @email toweidan@126.com
 */
public class ServicceA {
    public void test() {
        String v = ThreadHolder.get();
        System.out.println("ServiceA: " + v + ", Current: " + Thread.currentThread().getName());
    }
}

/**
 * A线程
 * @author liweidan
 * @date 2017.12.19 上午10:45
 * @email toweidan@126.com
 */
public class ThreadA implements Runnable {
    private ServicceA servicceA;
    public ThreadA(ServicceA servicceA) {
        this.servicceA = servicceA;
    }
    @Override
    public void run() {
        ThreadHolder.set("AThread value");
        System.out.println(ThreadHolder.get());
        new ServicceA().test();
    }
}

/**
 * B线程
 * @author liweidan
 * @date 2017.12.19 上午10:46
 * @email toweidan@126.com
 */
public class ThreadB implements Runnable {
    private ServicceA servicceA;
    public ThreadB(ServicceA servicceA) {
        this.servicceA = servicceA;
    }
    @Override
    public void run() {
        ThreadHolder.set("BThread value");
        System.out.println(ThreadHolder.get());
        new ServicceA().test();
    }
}

// 结果
AThread value
ServiceA: AThread value, Current: Thread-0
BThread value
ServiceA: BThread value, Current: Thread-1

通过开启两个不同的线程,但是注入相同的一个对象,在对象里面获得的值都是各自线程设置的,说明这个类是可以隔离各个线程之间的值,用于存放需要经常获取的值进行计算,以便于在同一个线程当中可以随处使用。

InheritableThreadLocal则可以在线程中创建子线程然后获取到对应的父线程的值。但是需要注意的是当子线程已经获取到值的时候,父线程对值进行修改,子线程并不会响应后面的修改,所以在项目中,最好是一旦设置了这个值就不要对其作出修改,以免业务发生错误。

六、总结

  1. 线程之间通过wait()以及notify()相互通知执行,需要注意唤醒的同类以免造成软件相互等待1
  2. 线程之间通过PipedStream进行数据通讯
  3. ThreadLocal的使用

[多线程]Java多线程03_线程同步及可见性

一、线程安全问题

线程安全问题是个经典的问题,主要涉及的方面就是对变量的修改以及读取。因为修改读取并不是一步完成,而计算机在切换线程的时候也没有清楚哪个时候可以切换,这时候就需要程序来给计算机指定道路说,你什么时候可以切换什么时候不行。特别是在对值修改的过程中,是不能够切换到读取线程的,要不然会出现脏读(即数据的错误)的问题。而Java对线程安全的控制主要是synchronized以及Lock锁。

关于变量安全控制的问题,我们现在大部分说到的变量安全问题均是类全局变量的安全问题,方法内部的局部变量,因为jvm在执行一个方法的时候都会进行线程栈之间的隔离,所以一个线程中对一个变量的修改创建并不会到另外一个线程对变量的修改创建。

(一)synchronized关键字

synchronized关键字主要用来修饰方法以及代码块,细节待会再说。被修饰的方法或者代码块只要有一个线程在执行,其他线程则必须等待该线程执行完毕,这就能够保证在赋值的时候,是赋值完成才切换给读取值的线程使用。并且可以通过Object类的wait()以及notify()方法进行线程之间的协调工作。但是细粒度并没有那么大,这也就是Lock类出现的理由。

(二)Lock锁

Lock值更能够细粒度的控制什么时候需要加锁,什么时候可以解锁,并且在线程之间通讯方面更加成熟,可以实现多个线程之间,指定线程的通讯。Lock锁放在后面才会说到

二、synchronized关键字

(一)基础用法和区别

示例代码

示例中一共有三个类:
– 图书类:代表图书,有一个方法setBookNameAndCode()是用来赋值的,也就是这个方法来测试线程安不安全
– 操作图书的业务层:用于创建不同的业务层给一个图书赋值
– 线程启动类:启动两个不同的业务层线程给一个共同的图书赋值

/**
 * 图书类
 * @author liweidan
 */
public class Book {
    /** 图书名字 */
    private String name;
    /** 图书编号 */
    private Integer bookCode;
    /**
     * 用于设置图书的名字和编号
     * @param name
     * @param code
     */
    public synchronized void setBookNameAndCode(String name, Integer code) {
        try {
            this.name = name;
            Thread.sleep(2000L);
            this.bookCode = code;
            System.out.println("bookName: " + this.name + ", code: " + this.bookCode);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    /** 省略getter setter toString */
}

/**
 * 图书业务类
 * @author liweidan
 * @date 2017.12.15 下午3:48
 * @email toweidan@126.com
 */
public class BookService implements Runnable {
    Book book;
    String name;
    Integer code;
    public BookService(Book book, String name, Integer code) {
        this.book = book;
        this.name = name;
        this.code = code;
    }
    @Override
    public void run() {
        book.setBookNameAndCode(name, code);
    }
}

public static void testSync01() {
    /** 创建一个共享资源 */
    Book book = new Book();
    /** 创建两个线程来对这个共享资源进行修改 */
    Thread t1 = new Thread(new BookService(book, "Java多线程", 10001), "Thread01");
    Thread t2 = new Thread(new BookService(book, "JVM虚拟机", 10002), "Thread02");
    t1.start();
    t2.start();

}

结果:

一共有两个结果:

    /**
     没有加synchronized:
        bookName: JVM虚拟机, code: 10001
        bookName: JVM虚拟机, code: 10001
     加synchronized:
         bookName: Java多线程, code: 10001
         bookName: JVM虚拟机, code: 10002
     */

1.没有加synchronized的情况

$seq
Thread01->共同的图书: this.name = “Java多线程”
Note right of Thread01:Thread01开始sleep
Thread02->共同的图书: this.name = “JVM虚拟机”
Note left of Thread02:Thread02开始sleep
Thread02->共同的图书: this.code = “10002”(Thread02抢到了CPU,先给赋值)
Thread01->共同的图书: this.code = “10001”
Thread01->共同的图书: 打印bookName: JVM虚拟机, code: 10001
Thread02->共同的图书: 打印bookName: JVM虚拟机, code: 10001
$

2.有加synchronized的情况

$seq
Thread01->共同的图书: 获得线程锁
Thread01->共同的图书: this.name = “Java多线程”
Note right of Thread01:Thread01开始sleep
Thread01->共同的图书: this.code = “10001”
Thread01->共同的图书: 打印bookName: Java多线程, code: 10001
Thread01->共同的图书: 释放线程锁
Thread02->共同的图书: 获得线程锁
Thread02->共同的图书: this.name = “JVM虚拟机”
Note left of Thread02:Thread02开始sleep
Thread02->共同的图书: this.code = “10002”
Thread02->共同的图书: 打印bookName: JVM虚拟机, code: 10002
Thread02->共同的图书: 释放线程锁
$

总结

可见,在需要同步的方法上加上synchronized关键字,可以起到让jvm按照顺序来执行的效果。当有多个线程进行访问的时候,这时候只能“排队”一个线程一个线程来执行方法,从而让方法安全。

另外一个重点是,在示例中一直强调一个资源,因为只有多个线程对同一个资源进行操作的时候才会出现线程安全问题,当我们创建两个资源,有两个线程分别操作的时候,根本就不会出现线程安全问题,synchronized关键字也不会让方法同步执行,因为不同的对象,是不同的锁,这时候操作依然会是异步操作。

(二)脏读问题

如果set方法同步了,但是get方法没有进行同步,那么get方法是会异步请求的。

执行过程:

$seq
设值线程->共同的图书: 获得线程锁
设值线程->共同的图书: this.name = “Java多线程”
Note right of 设值线程: 设值线程开始sleep
共同的图书->取值线程: 打印bookName: Java多线程, code:null
Note left of 取值线程: 因为取值没有同步所以进来了
设值线程->共同的图书: this.code = “10001”
设值线程->共同的图书: 释放线程锁
$

初步解决方式:给取值方法也加上synchronized,但是会引起性能问题。

这里有两个知识点:
1. 当一个同步方法被调用的时候,其他线程必须等待这个线程释放锁的时候才可以执行该方法或者其他synchronized方法
2. 当一个同步方法被调用的时候,另外一个方法如果没有同步,依然可以被异步调用

(三)锁重入

意思是:如果一个方法调用另外一个同步方法的时候,会立即获得锁,并且等这个方法执行完成以后才释放。

示例

修改setBookNameAndCode让他调用另外一个同步方法:

/**
 * 图书类
 * @author liweidan
 */
public class Book {
    /** 图书名字 */
    private String name;
    /** 图书编号 */
    private Integer bookCode;
    /**
     * 用于设置图书的名字和编号
     * @param name
     * @param code
     */
    public synchronized void setBookNameAndCode(String name, Integer code) {
        try {
            this.name = name;
            Thread.sleep(2000L);
            this.bookCode = code;
            anohtherMethod();
            System.out.println("bookName: " + this.name + ", code: " + this.bookCode);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public synchronized void anohtherMethod() {
        System.out.println("============ anohtherMethod ===========");
    }
}

结果

============ anohtherMethod ===========
bookName: Java多线程, code: 10001
============ anohtherMethod ===========
bookName: JVM虚拟机, code: 10002

(四)锁无继承性

当父类一个同步方法被子类重写的时候,依然需要加上同步标记

(五)总结

  1. 当方法加上synchronized关键字的时候,线程需要排队执行这个方法
  2. 当A线程调用一个同步方法的时候,B线程可以调用其他的非同步的方法
  3. 如果有两个同步方法,A线程在执行第一个方法的时候,B线程需要等待A线程的锁才可以进行执行另外一个同步方法
  4. 锁重入,当一个同步方法执行另外一个同步方法的时候,该线程可以立即获得锁继续执行
  5. 无继承性:子类重写父类同步方法的时候,需要再加上synchronized关键字

三、synchronized修饰代码块

(一)锁是对象自己的时候

synchronized方法的弊端:锁住整个方法,当方法中有些操作不需要同步的时候,也一起同步了,造成了不必要的性能损失。

synchronized方法的锁对象也是本对象,所以当我们用代码块锁锁住this的时候,其实性能是基本没有差别的。

示例

简单小示例来做一下,还是上面的例子,不过通过模拟耗时的不需要同步的操作来模仿后续

/**
 * 使用对象锁锁代码块
 * @author liweidan
 * @date 2017.12.15 下午5:15
 * @email toweidan@126.com
 */
public class BookObjLock {
    /** 图书名字 */
    private String name;
    /** 图书编号 */
    private Integer bookCode;
    /**
     * 用于设置图书的名字和编号
     * @param name
     * @param code
     */
    public void setBookNameAndCode(String name, Integer code) {
        int aNum = 0;
        for (int i = 0; i < 100000; i++) {
            // 其他事情
            aNum += i;
        }
        System.out.println(Thread.currentThread().getName() + ": " + aNum);
        synchronized (this) {
            try {
                this.name = name;
                Thread.sleep(2000L);
                this.bookCode = code;
                System.out.println("bookName: " + this.name + ", code: " + this.bookCode);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

/**
 * 图书业务类
 * @author liweidan
 * @date 2017.12.15 下午3:48
 * @email toweidan@126.com
 */
public class BookObjLockService implements Runnable {
    BookObjLock book;
    String name;
    Integer code;
    public BookObjLockService(BookObjLock book, String name, Integer code) {
        this.book = book;
        this.name = name;
        this.code = code;
    }
    @Override
    public void run() {
        book.setBookNameAndCode(name, code);
    }
}

public static void testObjLock() {
    /** 创建一个共享资源 */
    BookObjLock book = new BookObjLock();
    /** 创建两个线程来对这个共享资源进行修改 */
    Thread t1 = new Thread(new BookObjLockService(book, "Java多线程", 10001), "Thread01");
    Thread t2 = new Thread(new BookObjLockService(book, "JVM虚拟机", 10002), "Thread02");
    t1.start();
    t2.start();
}

结果

/** 这里是长操作,无同步 */
Thread01: 704982704
Thread02: 704982704
bookName: Java多线程, code: 10001
bookName: JVM虚拟机, code: 10002

可以看到,需要长时间的操作并不会同步执行。这样子就能大大提高项目的性能。

(二)锁是其他对象

代码块中,锁可以设置为其他对象,这样子就可以更进一步控制同步的时机。

示例

对设置namecode的方法加上不同的锁,以便在设置书名或者书编号的时候可以不被干扰。

public class BookObjLock {
    /** 图书名字 */
    private String name;
    /** 图书编号 */
    private Integer bookCode;
    /**
     * 用于设置图书的名字和编号
     * @param name
     * @param code
     */
    public void setBookNameAndCode(String name, Integer code) {
        int aNum = 0;
        for (int i = 0; i < 100000; i++) {
            // 其他事情
            aNum += i;
        }
        System.out.println(Thread.currentThread().getName() + ": " + aNum);
        synchronized (this) {
            try {
                this.name = name;
                Thread.sleep(2000L);
                this.bookCode = code;
                System.out.println("bookName: " + this.name + ", code: " + this.bookCode);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public Integer getBookCode() {
        return bookCode;
    }
    public void setBookCode(Integer bookCode) {
        synchronized ("code") {
            this.bookCode = bookCode;
            System.out.println("setBookCode============= name: " + this.name + ", code: "+this.bookCode + ", current" + System.currentTimeMillis());
            System.out.println(Thread.currentThread().getName() + "setBookCode over");
        }
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        synchronized ("name") {
            this.name = name;
            System.out.println("setName============= name: " + this.name + ", code: "+this.bookCode + ", current" + System.currentTimeMillis());
            System.out.println(Thread.currentThread().getName() + "setName over");
        }
    }
    @Override
    public String toString() {
        return "Book{" +
                "name='" + name + '\'' +
                ", bookCode=" + bookCode +
                '}';
    }
}

/** 两个线程 */
/**
 * 
 * @author liweidan
 * @date 2017.12.15 下午5:45
 * @email toweidan@126.com
 */
public class BookNameThread implements Runnable {
    BookObjLock book;
    String name;
    public BookNameThread(BookObjLock book, String name) {
        this.book = book;
        this.name = name;
    }
    @Override
    public void run() {
        book.setName(name);
    }
}

/**
 * 
 * @author liweidan
 * @date 2017.12.15 下午5:45
 * @email toweidan@126.com
 */
public class BookCodeThread implements Runnable {
    BookObjLock book;
    Integer code;
    public BookCodeThread(BookObjLock book, Integer code) {
        this.book = book;
        this.code = code;
    }
    @Override
    public void run() {
        book.setBookCode(code);
    }
}

public static void testMultiLock() {
    /** 创建一个共享资源 */
    BookObjLock book = new BookObjLock();
    Thread t1 = new Thread(new BookCodeThread(book, 10001), "t1-setCode");
    Thread t2 = new Thread(new BookCodeThread(book, 10002), "t2-setCode");
    Thread t3 = new Thread(new BookNameThread(book, "JVM虚拟机"), "t3-setName");
    t1.start();
    t2.start();
    t3.start();
}

结果

setBookCode============= name: null, code: 10001, current1513331932626
setName============= name: JVM虚拟机, code: 10001, current1513331932626
t1-setCodesetBookCode over
t3-setNamesetName over
setBookCode============= name: JVM虚拟机, code: 10002, current1513331932626
t2-setCodesetBookCode over

可以看到,设置code方法和设置name方法是异步执行的,只有是当两个线程抢到同一个锁的时候,才会同步执行。但是如果锁不是同一个,那么会是异步运行。

换句话说,通过控制不同方法锁不同的对象,可以控制线程执行不同方法的同步性。

注意:由于String对象具有一旦创建就放入常量池的特性,所以在加锁的时候,应该避免使用String作为线程锁,取而代之的是使用new Object()作为线程锁,并且这个对象不放入缓存中(即为了避免是同一个对象而让我们误以为是不同对象造成业务出错)

(三)嵌套锁与死锁

嵌套锁:就是一个同步代码块中,又有另外一个代码块。多用于一个方法修改两个对象,需要锁住不同对象的时候,但是其实开发中比较少遇到这种情况。

死锁:嵌套锁情况下,两个锁出现了互相等待,造成程序卡主无法执行下去。

示例

直接使用死锁来看嵌套锁的例子。

/**
 * 演示死锁
 * @author liweidan
 * @date 2017.12.16 下午2:35
 * @email toweidan@126.com
 */
public class DeadLine implements Runnable {
    private String userName;
    private Object lock1 = new Object();
    private Object lock2 = new Object();
    public void setUserName(String userName) {
        this.userName = userName;
    }
    @Override
    public void run() {
        if (userName.equals("a")) {
            synchronized (lock1) {
                doSomething();
                synchronized (lock2) {
                    System.out.println("Lock1 -> Lock2");
                }
            }
        } else if (userName.equals("b")) {
            synchronized (lock2) {
                doSomething();
                synchronized (lock1) {
                    System.out.println("Lock2 -> Lock1");
                }
            }
        }
    }
    private void doSomething() {
        try {
            System.out.println("username = " + userName);
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public static void testDeadlock() {
    try {
        DeadLine deadLine = new DeadLine();
        deadLine.setUserName("a");
        Thread t1 = new Thread(deadLine);
        t1.start();
        Thread.sleep(100);
        deadLine.setUserName("b");
        Thread t2 = new Thread(deadLine);
        t2.start();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

结果:

username = a
username = b

通过结果可以看到,两个线程卡住了,而程序并未运行结束,一直跑着。

执行时序图

$seq
线程t1->DeadLine: 创建了A的userName并且执行
线程t1->DeadLine: 获得Lock1
线程t1->DeadLine: 线程在run方法内睡眠3秒
线程2->DeadLine: 300秒被创建,B的userName并且执行
线程2->DeadLine: 获得Lock2
线程2->DeadLine: 线程在run方法内睡眠3秒
线程t1->DeadLine: 醒来,等待Lock2才能继续执行
线程2->DeadLine: 醒来,等待Lock1才能继续执行
Note right of DeadLine: 正是这一步导致死锁
$

四、volatile关键字

(一)概念

volatile修饰了变量后,当有多个线程拿到这个变量的时候,能够拿到最新的值。

jvm有主内存、线程栈的概念,当线程A改变了变量的值的时候,而线程B读取该值的时候,并不能够马上读到新的值,因为线程A改变的值并不会相应到线程B的线程栈中,要解决这个问题,加上volatile后,每次线程B取值的时候都会从公共堆中去取值。

接下来使用程序以及时序图来帮助理解

(二)具体示例

1. 未加上volatile

/**
 * @author liweidan
 * @date 2017.12.16 下午3:48
 * @email toweidan@126.com
 */
public class VolatileThread implements Runnable {
    private boolean isRunning = true;
    public void changeRunning() {
        isRunning = !isRunning;
    }
    public boolean isRunning() {
        return isRunning;
    }
    @Override
    public void run() {
        System.out.println("============== start ==============");
        while (isRunning) {
        }
        System.out.println("================= End =============");
    }
    public static void main(String[] args) {
        try {
            /** 创建一个资源 */
            VolatileThread thread = new VolatileThread();
            /** 启动一个线程执行run方法 */
            Thread t1 = new Thread(thread);
            t1.start();
            /** 修改变量的值 */
            Thread.sleep(3000L);
            thread.changeRunning();
            System.out.println(thread.isRunning);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

结果如下:

============== start ==============
false

2. 加上volatile

public class VolatileThread implements Runnable {
    private volatile boolean isRunning = true;
    .......
}

结果如下:

============== start ==============
false
================= End =============

可以看出来:
– 未加上volatile的时候,虽然主线程在3秒以后把isRunning进行了修改,但是线程停不下来。
– 加上volatile的时候,主线程在3秒以后把isRunning进行了修改,线程停下来了。

3. 分析结果差异

老方法,时序图分析过程。

对于未加上volatile的时候,程序是这么执行的:

$seq
线程t1->公共堆: 从主内存中读取isRunning变量
线程t1->线程栈: 写入isRunning=true
线程t1->线程栈: isRunning=true,写到线程栈,while循环
线程Main->公共堆: 等待3秒后,把isRunning改为false
线程Main->公共堆: 输出isRunning=false,结束
线程t1->线程栈: 读取线程栈isRunning=true,while一直循环
$

加上volatile的时候,程序是这么执行的:

$seq
线程t1->公共堆: 从主内存中读取isRunning变量
线程t1->线程栈: 读取isRunning=true,while开始循环
线程Main->公共堆: 等待3秒后,把isRunning改为false
线程Main->公共堆: 输出isRunning=false,结束
线程t1->公共堆: 读取线程栈isRunning= false,while停止
$

可以看出,当变量加上volatile的时候,那么强制线程栈每次读取变量的时候都从公共堆里面去读,所以能够读到最新的值。

(三)非原子性

1. 原子性问题

/**
 * 测试非原子性
 * @author liweidan
 * @date 2017.12.16 下午4:24
 * @email toweidan@126.com
 */
public class VolatileThread02 implements Runnable {
    private static int count;
    private static void addCount() {
        for (int i = 0; i < 100; i++) {
            count++;
        }
        System.out.println(count);
    }
    @Override
    public void run() {
        addCount();
    }
    public static void main(String[] args) {
        VolatileThread02[] ts = new VolatileThread02[100];
        Thread[] threads = new Thread[100];
        for (int i = 0; i < 100; i++) {
            ts[i] = new VolatileThread02();
            threads[i] = new Thread(ts[i]);
        }
        for (int i = 0; i < 100; i++) {
            threads[i].start();
        }
    }
}

/**  结果: */
....
9493
9593
9693
9793
9893
9993

结果并没有如我们所愿,一百一百的增加。所以我们需要给addCount()方法加上synchronized关键字才能让变量变成原子性的增加。

所以关键字volatile并不是说可以保证变量的安全,如需保证安全性依然需要使用synchronized关键字,关键字volatile只是为了保证变量在被取值的时候,都是最新的状态。

2. i++问题

上面讲到i++需要三个步骤:
a. 读取i的值
b. 新增
c. 写入主存

但是如果加上volatile关键字,是可以保证第a步,但是如果此时又被其他线程读取执行了第a步,那么就会出现脏数据

原子类AtomicXXX

AtomicXXX是类似于i++操作的原子类,后面的XXX可以替换成Java包装类的名字。

/**
 * 原子类Demo
 * @author liweidan
 * @date 2017.12.16 下午4:38
 * @email toweidan@126.com
 */
public class AtomicDemo extends Thread {
    private AtomicInteger count = new AtomicInteger(0);
    @Override
    public void run() {
        for (int i = 0; i < 10000; i++) {
            /** 新增并且获取 */
            System.out.println(count.incrementAndGet());
        }
    }
    public static void main(String[] args) {
        AtomicDemo atomicDemo = new AtomicDemo();
        Thread t1 = new Thread(atomicDemo);
        Thread t2 = new Thread(atomicDemo);
        Thread t3 = new Thread(atomicDemo);
        Thread t4 = new Thread(atomicDemo);
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

...
39994
39995
39996
39997
39998
39999
40000

结果可以安全的新增到40000

注意:虽然原子类是安全的,但是需要方法调用之间的安全性。

3. synchronized可以具备可见性

synchronized关键字可以是多线程访问同一个变量具有同步性,而且还具备将工作内存中的变量与公共内存中的变量同步的功能。

五、总结

(一)synchronized:

  1. 修饰方法可以让方法执行具有同步性,多个线程时需要依次逐个进入执行
  2. 修饰代码块,指定对象锁,当线程获取进入代码块的时候,其他线程需要等待进入
  3. 修饰代码块,指定多个对象锁,当线程获取进入代码块的时候,其他线程如需要进入同个代码块里,需要等待,但是如果进入的是另外一个锁的代码块,则可以和其他线程异步进行
  4. 脏读:当设值的时候进行了同步,但是可能设值和读取值的线程异步进行导致获取值是脏的数据。解决:给取值方法加上同步
  5. i++的原子性:读取、操作、赋值
  6. 锁重入:当一个加锁的方法或者代码块调用另外一个加锁的方法,可以立即获取锁
  7. 无继承性:重写父类同步方法需要重新声明

(二)volatile:

  1. 内存间的可见性
  2. 非原子性

[多线程]Java多线程02_线程操作

1. 线程常用方法

1)currentThread()

获取当前线程的对象信息,获取该对象就可以获取该线程的信息。常用方法是getName(),可以获取当前线程的名字。

  • 编写线程类
public class CurrentDemo implements Runnable {

    public CurrentDemo() {
        System.out.println("构造方法被调用,当前线程:" + Thread.currentThread().getName());
    }


    @Override

    public void run() {
        System.out.println("Run方法被调用,当前线程:" + Thread.currentThread().getName());
    }
}
  • 启动
public static void startVurrentDemo() {
    /** 由main调用构造方法 */
    CurrentDemo demo = new CurrentDemo();

    /** 由线程1调用run方法 */
    Thread t1 = new Thread(demo, "线程1");
    t1.start();
}
  • 结果
构造方法被调用,当前线程:main
Run方法被调用,当前线程:线程1

由结果可知,调用构造方法的也是一个线程,而且是main线程,而调用run方法的就是自己创建出来的一个线程了。

2)isAlive()

方法作用:用于测试线程是否处于活动状态。
活动状态:线程已经启动(调用start()方法),尚未结束。线程处于正在运行或者准备开始运行状态,则认为线程是“存活”的。

  • 线程类:
public class AliveDemo implements Runnable {

    public AliveDemo() {
        System.out.println("构造方法被调用");
        System.out.println("当前线程是:" + Thread.currentThread().getName());

        /** 注意调用当前的线程是Main,所以调用isAlive的时候返回的是true */
        System.out.println("isAlive: " + Thread.currentThread().isAlive());
        System.out.println("构造方法END");
    }

    @Override
    public void run() {
        System.out.println("RUN方法被调用");
        System.out.println("当前线程是:" + Thread.currentThread().getName());
        System.out.println("isAlive: " + Thread.currentThread().isAlive());
        System.out.println("RUN方法END");
    }
}
  • 启动类
public static void startIsAlive() {
    Thread t1 = new Thread(new AliveDemo(), "线程1");
    t1.start();
    System.out.println("Main: isAlive: " + t1.isAlive());
}
  • 结果
构造方法被调用
当前线程是:main
isAlive: true
构造方法END
RUN方法被调用
当前线程是:线程1
isAlive: true
RUN方法END
Main: isAlive: true

注意:在调用start的后面加上当前线程睡眠1秒钟,那t1输出的结果就不同了(变为false),因为在这个例子中,调用isAlive()的时候刚好线程还没运行结束

3)sleep()

方法作用:让当前线程沉睡指定的毫秒数。

  • 线程类:
public class SleepDemo implements Runnable {


    @Override
    public void run() {
        try {
            System.out.println("RUN开始,当前时间:" + System.currentTimeMillis());
            Thread.sleep(1000L);
            System.out.println("RUN结束,当前时间:" + System.currentTimeMillis());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  • 启动类
public static void startSleep() {
    Thread t1 = new Thread(new SleepDemo(), "睡眠线程");
    t1.start();
}
  • 结果
RUN开始,当前时间:1512970671432
RUN结束,当前时间:1512970672436

4)getId()

方法作用:获取线程唯一id值

2. 操作线程方法

1)停止线程

interrupt(): 给线程打上中断标记
interrupted(): 测试当前线程是否已经中断,执行后将状态标志设置为false(调用该方法后再调用第二次会返回false)
isInrrupted(): 测试线程Thread对象是否已经是中断状态,但不清除标志。

① interrupt()示例

  • 线程类
public class Stop01Demo implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 500000; i++) {
            System.out.println(i);
        }
    }
}
  • 启动类
public static void startStop01() {
    Thread t1 = new Thread(new Stop01Demo(), "测试停止标记");
    t1.start();
    try {
        Thread.sleep(200L);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    t1.interrupt();
}
  • 结果
...
499994
499995
499996
499997
499998
499999

根据结果来看,调用interrupt()并没有停止线程,而是为线程做了标记,配合interrupted()(1. 测试当前线程; 2. 有清除功能)和isInrrupted()(无清除功能)在run方法内部来做线程的退出。

② interrupted()示例

  • 线程类和启动类合一
public static void startInterrupted() {
    Thread.currentThread().interrupt();
    System.out.println("是否停止1:" + Thread.interrupted());
    System.out.println("是否停止2:" + Thread.interrupted());// 已经清除标记,所以返回 false

    /** 重新打上停止的标记 */
    Thread.currentThread().interrupt();
    System.out.println("是否停止3:" + Thread.interrupted());
}
  • 结果
是否停止1:true
是否停止2:false
是否停止3:true

③ isInterrupted()示例

  • 线程类
public class Stop02Demo implements Runnable {
    @Override
    public void run() {
        while (true) {
        }
    }
}
  • 启动类
public static void startStop02() {
    Thread t1 = new Thread(new Stop02Demo());
    t1.start();
    t1.interrupt();
    System.out.println("是否停止:" + t1.isInterrupted());
    System.out.println("是否停止:" + t1.isInterrupted());
}
  • 结果
是否停止:true
是否停止:true

那么结合isInterrupted()这个方法,我们可以在我们的逻辑里面做相关的退出操作:

  • 线程类:
public class Stop03Demo implements Runnable {
    @Override
    public void run() {
        while (true) {
            if (Thread.currentThread().isInterrupted()) {
                System.out.println("END!");
                return;
            }
        }
    }
}

④ 异常退出法

我们知道当线程发生异常的时候,如果没有使用try-catch进行捕获处理的话,那么线程是会退出运行的。那么只需要把上面的return改成抛出一个运行时异常即可,但是不知道为啥,我始终认为这个方法不够优雅。

⑤ sleep()和interrupt()

  • sleep()interrupt()
  • interrupt()sleep()
1. 先sleep()interrupt()
  • 线程类
public class SleepAndInterruptDemo implements Runnable {
    @Override
    public void run() {
        try {
            Thread.sleep(2000L);
        } catch (InterruptedException e) {
            System.out.println("InterruptedException!");
            e.printStackTrace();
        }
    }
}
  • 启动类
public static void startSleepAndInterrupt() {
    Thread t1 = new Thread(new SleepAndInterruptDemo());
    t1.start();
    t1.interrupt();
}
  • 结果
InterruptedException!
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at p02.threadClz.SleepAndInterruptDemo.run(SleepAndInterruptDemo.java:7)
    at java.lang.Thread.run(Thread.java:748)

结果刚好就是sleep方法需要捕获的异常

2. 先interrupt()sleep()
  • 线程类
public class InterruptAndSleepDemo implements Runnable {
    @Override
    public void run() {
        try {
            for (int i = 0; i < 100000; i++) {
                /** 拖延时间 */
                System.out.println(i);
            }
            System.out.println("Sleep");
            Thread.sleep(200000L);
            System.out.println("Sleep End");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  • 启动类
public static void startInterruptAndSleep() {
    Thread t1 = new Thread(new InterruptAndSleepDemo());
    t1.start();
    t1.interrupt();
}
  • 结果
...
99998
99999
Sleep
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at p02.threadClz.InterruptAndSleepDemo.run(InterruptAndSleepDemo.java:11)
    at java.lang.Thread.run(Thread.java:748)

就是睡不了

⑥ stop()示例

stop方法是一个已经被官方定义为过时的方法,因为这个方法是有缺陷的,它导致线程的后需清理工作做的不到位。所以在生产环境中禁止去使用stop方法进行线程的停止。

2)暂停线程

① 对应的方法有suspend()resume()

  • 线程类
public class SuspendDemo implements Runnable {
    public int i = 0;
    @Override
    public void run() {
        while (true) {
            i++;
        }
    }
}
  • 启动类
public static void startSuspendDemo() throws InterruptedException {
    SuspendDemo demo = new SuspendDemo();
    Thread t1 = new Thread(demo, "Suspend Thread");
    t1.start();

    /** 暂停运行 */
    t1.suspend();
    System.out.println("当前时间:" + System.currentTimeMillis() + ", i = " + demo.i);
    Thread.sleep(5000);
    System.out.println("当前时间:" + System.currentTimeMillis() + ", i = " + demo.i);

    /** 恢复运行 */
    t1.resume();
    Thread.sleep(5000);

    /** 再次暂停 */
    t1.suspend();
    System.out.println("当前时间:" + System.currentTimeMillis() + ", i = " + demo.i);
    Thread.sleep(5000);
    System.out.println("当前时间:" + System.currentTimeMillis() + ", i = " + demo.i);
}
  • 结果
当前时间:1512978809877, i = 205824
当前时间:1512978814881, i = 205824
当前时间:1512978819884, i = -1595043396
当前时间:1512978824887, i = -1595043396

② 占用同步资源

  • 资源类
public class SynchronizedObject {

    public synchronized void doSomething() {
        System.out.println("begin doSomething");
        if (Thread.currentThread().getName().equals("Thread01")) {
            System.out.println("暂停当前线程");
            Thread.currentThread().suspend();
        }
        System.out.println("END");
    }

}
  • 线程类
public class SuspendDemo2 implements Runnable {

    private SynchronizedObject synchronizedObject;

    public SuspendDemo2(SynchronizedObject synchronizedObject) {
        this.synchronizedObject = synchronizedObject;
    }

    @Override
    public void run() {
        synchronizedObject.doSomething();
    }
}
  • 运行类
public static void startSuspendDemo2() {
    SynchronizedObject synchronizedObject = new SynchronizedObject();
    Thread t1 = new Thread(new SuspendDemo2(synchronizedObject), "Thread01");
    Thread t2 = new Thread(new SuspendDemo2(synchronizedObject), "Thread02");

    t1.start();
    t2.start();

}
  • 结果
begin doSomething
暂停当前线程

可以看到t1线程在执行资源占用锁的时候,停止了,但是并没有把锁释放出来让其他线程用。

③ 另外,suspendresume会导致数据不一致

这个问题,跟资源有没有加锁是有关系的,但是加锁了会出现上面第②独占锁的情况,不加的话,资源不安全也是正常的,所以,不使用suspendresume才是正确的选择,后期会说到synchronize锁的问题,可以解决暂停以及重新恢复的问题。

3)让出资源

yield(): 让出当前CPU资源,但是可能刚刚让出资源,就又被该线程抢到了。

  • 线程类
public class YieldDemo implements Runnable {

    @Override
    public void run() {
        long start = System.currentTimeMillis();
        int count = 0;
        for (int i = 0; i < 50000000; i++) {
            /** 这里指定一个线程名字用来让出CPU */
            if (Thread.currentThread().getName().equals("YieldThread")) {
                Thread.yield();
            }
            count += i;
        }
        System.out.println(Thread.currentThread().getName() + "耗时:" + (System.currentTimeMillis() - start));
    }

}
  • 启动类
public static void startYield() {
    YieldDemo demo = new YieldDemo();
    Thread t1 = new Thread(demo, "YieldThread");
    Thread t2 = new Thread(demo, "YieldThread2");
    t1.start();
    t2.start();
}
  • 结果
YieldThread2耗时:120
YieldThread耗时:11132

可以看出来,让出CPU的线程执行的非常慢。

4)优先级

通过线程对象调用setPriority(int newPriority)来设置线程的优先级。优先级的范围是1-10,如果不是就会抛出异常。

需要注意:并不是优先级高的任务会先执行完,而是说优先级高的线程会更有机会获得CPU时间片,从而更快的执行相对应的任务。

特性:
– 继承
– 规则性
– 随机性

通过优先级来模拟龟兔赛跑

  • 动物类
public class Animal implements Runnable {
    @Override
    public void run() {
        for (int j = 0; j < 1000000; j++) {
            System.out.println(Thread.currentThread().getName());
        }
        System.out.println(Thread.currentThread().getName() + "跑完了100米");
    }
}
  • 线程
public static void rabbitAndTurtle() {
    Thread t1 = new Thread(new Animal(), "兔子");
    Thread t2 = new Thread(new Animal(), "乌龟");

    t1.setPriority(Thread.MAX_PRIORITY);// 兔子比较快
    t2.setPriority(Thread.MIN_PRIORITY);// 乌龟比较慢

    t2.start();
    t1.start();
}
  • 结果

结果太长了,但是每次运行“均”都是龟比较慢跑完

3. 守护线程

守护线程:当主线程退出运行的时候,启动的线程也就退出运行。如:GC(垃圾回收器),当Java线程退出的时候,它的存在也就失去了意义,所以他也退出了运行。默认是关闭的。

  • 线程类
public class DaemonDemo implements Runnable {
    @Override
    public void run() {
        while (true) {
            try {
                System.out.println("当前时间是: " + LocalDate.now());
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
  • 启动类
public static void startDaemon() {
    Thread t1 = new Thread(new DaemonDemo(), "守护线程");
    t1.setDaemon(true);
    t1.start();
    try {
        Thread.sleep(5000L);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}
  • 结果
当前时间是: 2017-12-11
当前时间是: 2017-12-11
当前时间是: 2017-12-11
当前时间是: 2017-12-11
当前时间是: 2017-12-11

当设置了t1.setDaemon(true);的时候,主线程退出运行,则其启动的线程也退出了运行。

[多线程]Java多线程01_初入门

1. 线程与进程的区别

进程 > 线程

进程:相当于启动一个软件,就启动了一个进程。当启动了QQ以及微信,那么这两个不同软件运行的独立任务就是两个不同的进程

线程:一个软件了里面,为了进行多个任务,就需要多个线程来协同工作,以可以尽可能多的使用计算机的资源。

注:由于CPU在线程、进程之间切换非常快,所以给人一种这些进程、线程都是在一起工作的错觉。

2. Java中的线程使用

Java是一门多线程的语言,而且,不论在安卓开发也好、服务器开发也好、桌面开发也好,都很多的使用了线程来完成多个任务。

安卓开发:使用多个线程,来执行多个任务,比如页面的同时渲染,在发送数据的时候确保Socket连接等等

服务器开发:最常见的SpringMVC开发,其实整个服务器只实例化一个DispatcherServlet,而能够支持多个用户同时在线操作的原因就是因为,SpringMVC使用了线程来对数据进行很好的隔离,从而使数据都是安全的。

桌面开发:跟安卓差不多。

需要说明的是,我们知道,Java程序的入口是main方法,而main方法本身就是一个线程,具体可以通过在main方法中调用currentThread()获取相对应的线程然后再通过getName()方法获取对应的名字,可以在控制台打印出来得到main的字符串

Java中使用多线程也是非常方便的,有两种情况,一种是继承Thread类,另外一种就是实现Runnable接口

1)继承Thread类

  • 编写线程类
package p01.threadClass;

/**
 * 通过继承Thread方式开发线程
 */
public class ExtendThread extends Thread {

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

  • 启动
public static void startExtThread() {
    ExtendThread extendThread1 = new ExtendThread();
    ExtendThread extendThread2 = new ExtendThread();
    ExtendThread extendThread3 = new ExtendThread();

    extendThread1.setName("Thread-01");
    extendThread2.setName("Thread-02");
    extendThread3.setName("Thread-03");

    extendThread1.start();
    extendThread2.start();
    extendThread3.start();
}
  • 打印:
Thread-01
Thread-03
Thread-02

这样就可以编写自己的线程任务了。

需要注意的是:
1. 虽然我们在main方法中按顺序调用三个线程,但并不意味着这三个线程会按照顺序运行,而是通过抢夺资源,哪个线程抢到了,就先运行,所以控制台打印的顺序也没有按照启动的顺序来;
2. 因为Java是单继承的关系,所以,并不推荐使用继承的方式来开发,而是通过下面实现接口的方式来做。

2)实现Runnable接口(推荐使用)

  • 编写线程类
package p01.threadClass;

/**
 * 实现Runable
 */
public class ImplementThread implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

  • 启动
public static void startImplThread() {
    /** 需要通过传入我们的实现的线程的对象来创建线程 */
    Thread thread1 = new Thread(new ImplementThread());
    Thread thread2 = new Thread(new ImplementThread());
    Thread thread3 = new Thread(new ImplementThread());

    thread1.setName("Thread-01");
    thread2.setName("Thread-02");
    thread3.setName("Thread-03");

    thread1.start();
    thread2.start();
    thread3.start();
}
  • 控制台
Thread-01
Thread-02
Thread-03

效果和继承是一样的,只不过通过实现,在Java开发思想中更加符合,也能规避单继承的缺陷。

3. 初识线程安全

出现线程安全问题,我们需要让多个线程共享同一个变量,当多个线程读取变量的时候,如果没有对该变量做任何限制,则会出现线程安全的问题。

  • 线程类
public class ThreadSafeDemo extends Thread {

    private int n = 5;

    @Override
    public void run() {
        n--;
        System.out.println("由" + this.currentThread().getName() + "执行,count = " + n);
    }   
}
  • 启动类
public static void startThreadSafeDemo() {
    ThreadSafeDemo demo = new ThreadSafeDemo();
    Thread th1 = new Thread(demo, "A");
    Thread th2 = new Thread(demo, "B");
    Thread th3 = new Thread(demo, "C");
    Thread th4 = new Thread(demo, "D");
    Thread th5 = new Thread(demo, "E");

    th1.start();
    th2.start();
    th3.start();
    th4.start();
    th5.start();
}
  • 结果
由B执行,count = 2
由D执行,count = 1
由A执行,count = 2
由C执行,count = 2
由E执行,count = 0

结果得到的是一个全部乱套了的数据.

原因:i--不是一个原始性操作,在jvm内部,i--分为三步运行:

1. 取得原有i的值
2. 计算i-1
3. 对i进行赋值

那么在这个过程中,可能线程A在执行第一步和第二步的时候,CPU资源就被线程B抢走了,导致A对i进行了减一操作,但是还没把值赋回去,而线程B获取的值是还没减一操作的值,所以这个过程导致了线程安全的问题。

1)初步解决方法

run方法加上synchronized关键字,可以解决,但是速度较慢,后续会慢慢根据这个思路进行优化。

  • 线程类代码片段
@Override
public synchronized void run() {
    n--;
    System.out.println("由" + this.currentThread().getName() + "执行,count = " + n);
}
  • 结果
由A执行,count = 4
由B执行,count = 3
由C执行,count = 2
由D执行,count = 1
由E执行,count = 0

原因:加上synchronized关键字,可以保证一个线程在该方法内部进行计算的时候,不能被其他线程抢走资源,也就是说,当线程A在执行i--上面那三个操作的时候,只能等到线程A执行完成,才能被其他线程获取。

4. 总结

到这里就已经把Java开发线程基本步骤给学完了,接下来就是要管理线程的生命周期。例如启动、暂停、停止等等,还有一系列的线程安全问题。

打算开始写数据结构相关的东西

其实我作为一个开发工程师但是本身并不是计算机专业出身的,这让我在应聘的时候有点折扣,好在现在还有自学考试这个东西,所以我报名了自考。

学数据结构这个东西,也不是自考趋势我去看,因为在很多网站,很多高手看来,像计算机专业的一些课程比如操作系统、数据结构等等,都是比较重要的,所以带着这个来做。

数据结构在我看来,就是在平时编码过程中可以找到解决问题的最优方式(可能不是最优,但是比较好的),所以要说不重要那是不可能的,就像高中的时候做数学题,找到最优方式,可以省点笔墨,维护起来也比较舒服。

所以带着以前老师常说的4W1H来学这个东西。也是我最近想起来的一个东西,在写一个知识点的时候先记录这几个规则,会让知识点显得更清晰。

使用自考带的教科书籍,难受,看都没看懂,主要是语法格式也是让我难受的一部分,所以直接使用外面推荐的书籍《大话数据结构》,这本书写的很好,通俗易懂,感觉也是老师在上课的时候的讲话实录,有一种自己在上课的感觉。所以写数据结构的时候我就使用这本书来看,配图也是截图出来的。