chengfengjie / chengfengjie.github.io

我叫MT
1 stars 0 forks source link

多线程(线程安全) #14

Open chengfengjie opened 6 years ago

chengfengjie commented 6 years ago

多线程编程中的三个核心概念

1、原子性: 一个操作要么全部执行,要么全部不执行。 2、可见性: 当多个线程并发访问共享变量时,一个线程对共享变量的修改,其他线程能够立即看到。 解释: CPU从主内存中读取数据的效率相对来说并不高,现在主流的计算机都有几级缓存。每个线程读取共享变量时,都会将变量加载进对应CPU的高速缓存里,修改变量后,CPU会立即更新该缓存,但是并不一定会立即将其写入主内存(实际上写会主内存的时间不可预期)。此时其他线程(尤其是不在同一CPU上执行的线程)访问变量时,从主内存中读到的就是旧的数据,而非第一线程更新后的数据。 3、顺序性: 程序执行的顺序按照代码的先后顺序执行

chengfengjie commented 6 years ago

线程安全

多线程

多线程: 指的是一个程序运行时产生了不止一个线程

并行和并发

线程安全

经常用来描述一段代码,在并发的情况之下,该代码经过多线程使用,线程的调度顺序不影响任何结果。这个时候使用多线程,我们只需要关注系统的内存、CPU是否够用即可。反过来,线程不安全就意味着线程的调度顺序会影响最终结果。

同步

Java中的同步指的是人为的控制和调度,保证共享资源的对线程访问成为线程安全,来保证结果的准确。

chengfengjie commented 6 years ago

wait、notify、notifyAll

在Java中可以用waitnotifynotifyAll来实现线程间的通信。 我们可以用wait()来让一个线程在某些条件下暂停运行。

在调用wait()notify()notifyAll()的时候,必须先获得锁,且状态变量须有锁保护,而固有锁对象与固有条件队列对象又是同一个对象。也就是说,要在某个对象上执行waitnotify,必须先锁定该对象,而对应的状态变量也是由该对象锁保护的

执行waitnotify不获取锁会抛出异常 java.lang.IllegalMonitorStateException

典型的生产者和消费者的代码

package huijinhang.hms.console;

import java.util.LinkedList;
import java.util.Queue;
import java.util.Random;

public class ProducerConsumerInJava {

    public static void main(String args[]) {
        ProducerConsumerInJava app = new ProducerConsumerInJava();
        app.runTest();
    }

    void runTest() {
        Queue<Integer> buffer = new LinkedList<>();
        int maxSize = 10;
        Thread producer = new Producer(buffer, maxSize, "PRODUCER");
        Thread consumer = new Consumer(buffer, maxSize, "CONSUMER");

        producer.start();
        consumer.start();
    }

    class Producer extends Thread {
        private Queue<Integer> queue;
        private int maxSize;
        public Producer(Queue<Integer> queue, int maxSize, String name) {
            super(name);
            this.queue = queue;
            this.maxSize = maxSize;
        }

        @Override
        public void run() {
            while (true) {
                synchronized (queue) {
                    while (queue.size() == maxSize) {
                        try {
                            System.out.println("Queue is full");
                            queue.wait();
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                    Random random = new Random();
                    int i = random.nextInt();
                    System.out.println("Producing value : " + i);
                    queue.add(i);
                    queue.notifyAll();
                }
            }
        }
    }

    class Consumer extends Thread {
        private Queue<Integer> queue;
        private int maxSize;
        public Consumer(Queue<Integer> queue, int maxSize, String name) {
            super(name);
            this.queue = queue;
            this.maxSize = maxSize;
        }

        @Override
        public void run() {
            while (true) {
                synchronized (queue) {
                    while (queue.isEmpty()) {
                        System.out.println("Queue is empty, Consumer thread is wait");
                        try {
                            queue.wait();
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("Consuming value: " + queue.remove());
                    queue.notifyAll();
                }
            }
        }
    }
}
chengfengjie commented 6 years ago

如果当多个线程访问同一个可变的状态变量时没有使用合适的同步,那么程序就会出现错误。有三种方式可以修复这个问题:

当设计线程安全的类时,良好的面向对象技术、不可修改性,以及明确的不可变规范都能起到一定的帮助作用。

chengfengjie commented 6 years ago

无状态对象一定是线程安全的

大多数的Servlet都是无状态的,从而极大低降低了在实现servlet线程安全性时的复杂性。当只有Servlet处理请求时需要保存一些信息,线程安全才会成为一个问题。

竞态条件

在并发编程中,由于不恰当的执行时序而出现不正确的结果是一种非常重要的情况,它的正式名字就叫做:静态条件

原子操作

假定有两个操作A和B,如果从执行线程A的线程来看,当另一个线程B执行时,要么将B全部执行完毕,要么完全不执行B,那么A和B对彼此来说是原子的。原子操作是指,对于访问同一个状态的所有操作(包括操作本身)来说,这个操作是一个以原子方式执行的操作

chengfengjie commented 6 years ago

一个非线程安全的例子

class Counter {
    private long count = 0;

    void increment() {
        this.count ++;
    }

    long getCount() {
        return this.count;
    }
}

class CounterThread extends Thread {

    private Counter counter;

    public CounterThread(Counter counter) {
        super();
        this.counter = counter;
    }

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            this.counter.increment();
        }
        System.out.println(counter.getCount());
    }
}

public class AtomicMain {

    public static void main(String args[]) {

        Counter counter = new Counter();

        CounterThread thread1 = new CounterThread(counter);
        CounterThread thread2 = new CounterThread(counter);

        thread1.start();
        thread2.start();
    }

}

这段程序的执行结果完全不可控

chengfengjie commented 6 years ago

重入

当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。由于内置锁时可重入的,如果某个线程试图获得一个由它自己持有的锁,那么这个请求就会成功。

如果内置锁是不可重入的,那么下面这段代码将发生死锁

public class Widget {
    public synchronized void doSomething() {
    }
}

public class LoggingWidget extend Widget {
    public synchronized void doSomething() {
        super.doSomething();
    }
}

对于可能被多个线程同时访问的可变状态变量,在访问他时都需要持有一个锁,在这种情况下,我们称状态变量是由这个锁保护的

对于每个包含多个变量的不变性条件,其中涉及的所有变量都需要由同一个锁来保护