Java多线程(下)

线程同步

当多个线程访问一个对象时,有可能会发生污读,即读取到未及时更新的数据,这个时候就须要线程同步。html

线程同步:java

即当有一个线程在对内存进行操做时,其余线程都不能够对这个内存地址进行操做,直到该线程完成操做, 其余线程才能对该内存地址进行操做,而其余线程又处于等待状态,实现线程同步的方法有不少,临界区对象就是其中一种。git

在通常状况下,建立一个线程是不能提升程序的执行效率的,因此要建立多个线程。可是多个线程同时运行的时候可能调用线程函数,在多个线程同时对同一个内存地址进行写入,因为CPU时间调度上的问题,写入数据会被屡次的覆盖,因此就要使线程同步。算法

同步就是协同步调,按预约的前后次序进行运行。如:你说完,我再说。编程

“同”字从字面上容易理解为一块儿动做安全

其实不是,“同”字应是指协同、协助、互相配合。多线程

如进程、线程同步,可理解为进程或线程A和B一块配合,A执行到必定程度时要依靠B的某个结果,因而停下来,示意B运行;B依言执行,再将结果给A;A再继续操做。并发

所谓同步,就是在发出一个功能调用时,在没有获得结果以前,该调用就不返回,同时其它线程也不能调用这个方法。按照这个定义,其实绝大多数函数都是同步调用(例如sin, isdigit等)。可是通常而言,咱们在说同步、异步的时候,特指那些须要其余部件协做或者须要必定时间完成的任务。例如Window API函数SendMessage。该函数发送一个消息给某个窗口,在对方处理完消息以前,这个函数不返回。当对方处理完毕之后,该函数才把消息处理函数所返回的LRESULT值返回给调用者。异步

在多线程编程里面,一些敏感数据不容许被多个线程同时访问,此时就使用同步访问技术,保证数据在任什么时候刻,最多有一个线程访问,以保证数据的完整性。ide

因为同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题,为了保证数据在方法中被访问时的正确性,在访问时加入锁机制synchronized,当一个线程得到对象的排它锁,独占资源,其余线程必须等待,使用后释放锁便可能存在如下问题:

  • 一个线程持有锁会致使其余全部须要此锁的线程挂起;
  • 在多线程竞争下,加锁,释放锁会致使比较多的上下文切换和调度延时,引
    起性能问题;
  • 若是一个优先级高的线程等待- -个优先级低的线程释放锁会致使优先级倒
    置,引发性能问题.

举个例子,一个售票口有10张票,当100我的同时去买时,每一个人都获取到了有100张票的数据,因此每一个人买了一张,致使最后剩下-90张票,线程不一样步就会致使这种结果。

synchronized

synchronized是Java中的关键字,是一种同步锁。它修饰的对象有如下几种:

  1. 修饰一个代码块,被修饰的代码块称为同步语句块,其做用的范围是大括号{}括起来的代码,做用的对象是调用这个代码块的对象;
  2. 修饰一个方法,被修饰的方法称为同步方法,其做用的范围是整个方法,做用的对象是调用这个方法的对象;
  3. 修改一个静态的方法,其做用的范围是整个静态方法,做用的对象是这个类的全部对象;
  4. 修改一个类,其做用的范围是synchronized后面括号括起来的部分,做用主的对象是这个类的全部对象。

咱们写一个例子,使用线程不安全的List来看看效果

public class MyThread{
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            new Thread(()->{
               list.add(Thread.currentThread().getName());
            }).start();
        }
        Thread.sleep(2000);
        System.out.println(list.size());
    }
}

能够看到,循环1000次,只存进去998个,重复执行,这个大小还会变化,因此是线程不安全的。

可使用synchronized把list加锁,就能保证每次都能插入进去。

public class MyThread{
    public static void main(String[] args) throws InterruptedException {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            new Thread(()->{
               synchronized (list) {
                   list.add(Thread.currentThread().getName());
               }
            }).start();
        }
        Thread.sleep(2000);
        System.out.println(list.size());
    }
}

这样就可以保证线程安全。

也可使用JUC(java.util.concurrent)包下的线程安全的列表CopyOnWriteArrayList,代码以下

import java.util.concurrent.CopyOnWriteArrayList;

public class MyThread{
    public static void main(String[] args) throws InterruptedException {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 1000; i++) {
            new Thread(()->{
               list.add(Thread.currentThread().getName());
            }).start();
        }
        Thread.sleep(2000);
        System.out.println(list.size());
    }
}

使用CopyOnWriteArrayList就能够不须要synchronized关键字实现线程安全

查看源代码能够发现,CopyOnWriteArrayList实现了List<E>接口

而后再add方法中使用了synchronized来加锁,和咱们上面的操做方法一致

//CopyOnWriteArrayList中的add()方法
public boolean add(E e) {
    synchronized (lock) {
        Object[] es = getArray();
        int len = es.length;
        es = Arrays.copyOf(es, len + 1);
        es[len] = e;
        setArray(es);
        return true;
    }
}

死锁

所谓死锁,是指多个进程在运行过程当中因争夺资源而形成的一种僵局,当进程处于这种僵持状态时,若无外力做用,它们都将没法再向前推动。

死锁的条件

  • 互斥条件
  • 请求和保持
  • 不可抢占
  • 循环等待

只要破坏后三个条件之一就能够避免死锁,可使用银行家算法等方法。

Lock锁

  • 从JDK 5.0开始,Java提供了更强大的线程同步机制一经过显式定义同步锁对象来实现同步。同步锁使用Lock对象充当java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。
  • Lock锁提供了对共享资源的独占访问,每次只能有一个线程对Lock对象加锁,线程开
    始访问共享资源以前应先得到Lock对象
  • ReentrantLock类实现了Lock,它拥有与synchronized相同的并发性和内存语义,在实现线程安全的控制中,比较经常使用的是ReentrantLock,能够显式加锁、释放锁。

先写一个不使用锁的例子

import java.util.concurrent.locks.ReentrantLock;

public class MyThread implements Runnable {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        Thread thread1 = new Thread(thread);
        Thread thread2 = new Thread(thread);
        Thread thread3 = new Thread(thread);

        thread1.start();
        thread2.start();
        thread3.start();

    }
    public static int tickets = 10;
    @Override
    public void run() {
        while (true) {
            if (tickets > 0) {
                System.out.println(tickets--);
            } else {
                break;
            }
        }
    }
}

执行后发现顺序彻底是乱的

使用ReentrantLock(可重入锁)来把相关代码加锁,便可实现按顺序调用

import java.util.concurrent.locks.ReentrantLock;

public class MyThread implements Runnable {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        Thread thread1 = new Thread(thread);
        Thread thread2 = new Thread(thread);
        Thread thread3 = new Thread(thread);

        thread1.start();
        thread2.start();
        thread3.start();

    }
    public static int tickets = 10;
    final ReentrantLock lock = new ReentrantLock();
    @Override
    public void run() {
        while (true) {
            try {
                lock.lock();
                if (tickets > 0) {
                    System.out.println(tickets--);
                } else {
                    break;
                }
            } finally {
                lock.unlock();
            }
        }
    }
}

这样也能够实现线程同步。

  • Lock是显式锁(手动开启和关闭锁,别忘记关闭锁) synchronized是隐式锁,出了
    做用域自动释放
  • Lock只有代码块锁,synchronized有代码块锁和方法锁
  • 使用Lock锁, JVM将花费较少的时间来调度线程,性能更好。而且具备更好的扩展
    性(提供更多的子类)。
  • 优先使用顺序:
    • Lock >同步代码块(已经进入了方法体,分配了相应资源) >同步方法(在方
      法体以外)

线程通讯

生产者和消费者问题

  • 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取走消费。
  • 若是仓库中没有产品,则生产者将产品放入仓库,不然中止生产并等待,直到仓库中的产品被消费者取走为止。
  • 若是仓库中放有产品,则消费者能够将产品取走消费,不然中止消费并等待,直到仓库中再次放入产品为止。

Java提供的线程通讯方法

方法名 做用
wait() 表示线程一直等待,直到其余线程通知,与sleep不一样,会释放锁
wait(long timeout) 指定等待的毫秒数
notify() 唤醒一个处于等待状态的线程
notifyAll() 唤醒同一个对象上全部调用wait()方法的线程,优先级别高的线程优先调度

均是0bject类的方法都,只能在同步方法或者同步代码块中使用,不然会抛出llegalMonitorStateException

  • 对于生产者,没有生产产品以前,要通知消费者等待.而生产了产品以后,又须要马_上通知消费者消费
  • 对于消费者,在消费以后,要通知生产者已经结束消费,须要生产新的产品以供消费
  • 在生产者消费者问题中,仅有synchronized是不够的
    • synchronized 可阻止并发更新同- -个共享资源,实现了同步
    • synchronized 不能用来实现不一样线程之间的消息传递(通讯)

解决方式一:管程

首先定义一个生产者类

//生产者
class Producer extends Thread {
    SynContainer container;
    public Producer(SynContainer container) {
        this.container = container;
    }

    //生产
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("生产第" + i + "个");
            container.push(new Product(i));
        }
    }
}

生产者不断往缓冲区添加产品,而后定义一个消费者类

//消费者
class Consumer extends Thread {
    SynContainer container;
    public Consumer(SynContainer container) {
        this.container = container;
    }

    //消费
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("消费第" + container.pop().id + "个");
            try {
                Thread.sleep(500);
            } catch (InterruptedException ignored) { }
        }
    }
}

消费者不断在缓冲区去除产品,这里添加一个sleep来模拟真实效果

最后定义缓冲区

//缓冲区
class SynContainer {
    //容器大小
    Product[] products = new Product[10];
    //计数器
    int count = 0;

    //生产者放入产品
    public synchronized void push(Product product) {
        //若是满了,通知消费者,生产者等待,不然放入产品
        if (count == products.length) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        products[count++] = product;
        this.notifyAll();
    }
    //消费者消费产品
    public synchronized Product pop() {
        if (count == 0) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.notifyAll();
        return products[--count];
    }
}

缓冲区的两个方法都是使用synchronized修饰,保证可以执行完整,而后根据容器大小来判断是否让生产者以及消费者线程等待

当容器中没有产品时,通知消费者等待,生产者线程开始,当产品满时,通知生产者等待,消费者线程开始。

最后补上产品类

//产品
class Product {
    //产品编号
    int id;

    public Product(int id) {
        this.id = id;
    }
}

解决方式二:信号量

类定义和上面相似,只不过在产品类中添加了一个信号量来区分是否有产品,不须要一个缓冲区

//生产者
class Producer extends Thread {
    Product product;

    public Producer(Product product) {
        this.product = product;
    }

    //生产
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            this.product.push("产品" + i);
        }
    }
}

//消费者
class Consumer extends Thread {
    Product product;

    public Consumer(Product product) {
        this.product = product;
    }

    //消费
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            this.product.pop();
        }
    }
}

//产品
class Product {
    String product;
    boolean flag = true;

    //生产
    public synchronized void push(String product) {
        if (!flag) {
            try {
                this.wait();
            } catch (InterruptedException ignored) { }
        }
        System.out.println("生产了" + product);
        //通知消费
        this.notifyAll();
        this.product = product;
        this.flag = !this.flag;

    }

    //消费
    public synchronized void pop() {
        if (flag) {
            try {
                this.wait();
            } catch (InterruptedException ignored) { }
        }
        System.out.println("消费了" + this.product);
        //通知生产者
        this.notifyAll();
        this.flag = !this.flag;
    }
}

这样也能够解决生产者和消费者问题

线程池

背景

常常建立和销毁、使用量特别大的资源,好比并发状况下的线程,对性能影响很大。

思路:提早建立好多个线程,放入线程池中,使用时直接获取,使用完放回池中。能够避免频繁建立销毁、实现重复利用。相似生活中的公共交通工具。

优势

  • 提升响应速度(减小了建立新线程的时间)
  • 下降资源消耗(重复利用线程池中线程,不须要每次都建立)
  • 便于线程管理

参数说明

  • corePoolSize: 核心池的大小
  • maximumPoolSize:最大线程数
  • keepAliveTime: 线程没有任务时最多保持多长时间后会终止

JDK 5.0起提供了线程池相关API: ExecutorService和Executors
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor

  • void execute(Runnable command) :执行任务/命令,没有返回值,-般用来执行Runnable
  • <T> Future<T> submit(Callable<T> task):执行任务,有返回值,一-般 又来执行
    Callable
  • void shutdown() :关闭链接池
    Executors:工具类、线程池的工厂类,用于建立并返回不一样类型的线程池

代码演示

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Test {
    public static void main(String[] args) {
        //建立线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        service.execute(new MyThread());
        service.execute(new MyThread());
        service.execute(new MyThread());

        //关闭链接
        service.shutdown();
    }
}

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

这样就能够实现经过线程池来管理线程

总结

  • 线程就是独立的执行路径;
  • 在程序运行时,即便没有本身建立线程,后台也会有多个线程,如主线程,gc线程;
  • main()称之为主线程,为系统的入口,用于执行整个程序;
  • 在一个进程中,若是开辟了多个线程,线程的运行由调度器安排调度,调度器是与
  • 操做系统紧密相关的,前后顺序是不能认为的干预的。
  • 对同一份资源操做时,会存在资源抢夺的问题,须要加入并发控制;
  • 线程会带来额外的开销,如cpu调度时间,并发控制开销。
  • 每一个线程在本身的工做内存交互,内存控制不当会形成数据不一致

Java多线程(上)http://www.javashuo.com/article/p-cjqayqvb-wr.html

查看原文

相关文章
相关标签/搜索