Java之多线程

55 篇文章 0 订阅
33 篇文章 0 订阅

一、进程(Porcess)

       进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础,每个进程在执行过程中拥有独立的内存单元在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器,由程序、数据和进程控制块三部分组成。程序是指令、数据及其组织形式的描述,而进程是程序的实体。在Java中理解为调用一次main()方法时的整个程序的运行活动。

     1、特性: 

       动态性:进程的实质是程序在多道程序系统中的一次执行过程,进程是动态产生,动态消亡的;

       并发性:任何进程都可以同其他进程一起并发执行;

       独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位;

       异步性:由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推进

     2、主要状态:

       就绪状态(Ready):进程已获得除CPU外所需的资源,万事俱备只欠东风。等待分配到CPU资源,只要分配到CPU时进程就可执行。就绪进程可以按多个优先级来划分队列。例如,当一个进程由于时间片用完而进入就绪状态时,排入低优先级队列;当进程由I/O操作完成而进入就绪状态时,排入高优先级队列。

       运行状态(Running):进程占用CPU资源。处于此状态的进程的数目小于等于CPU的数目。在没有其他进程可以执行时(比如所有进程都在阻塞状态),通常会自动执行系统的空闲进程。

       阻塞状态(Blocked):由于进程等待某种前置依赖条件(如I/O操作或进程同步),在条件满足之前无法继续执行。该事件发生前即使把处理器资源分配给该进程,也无法运行。

二、线程(Thread):

       线程被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。多个线程共享他们所在进程中的某些内存。一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行。

       如果把CPU看成是电力,把一台计算机看成一个工厂,把每个进程看成是工厂的每个车间,把每个线程看成是车间里的每个工人。那么:

1、电力(CPU)给每个车间(进程)供电,各车间可以同时运行,它们之间互不影响;

2、假定由于电力不足(单核CPU),不能让所有车间(进程)同时运转,只能让一个车间运转。也就是说一个车间(进程)运转时其他车间(进程)必须得歇着,即单核CPU任一时刻只能运行一个任务(进程),其他进程处于非运行状态;

3、车间(进程)中有很有工人(线程),他们都要使用车间的设备(内存空间)来完成工作,而设备都是要用电的;

4、每个车间(进程)中的设备和电力是工人(线程)共享的,在同一个车间的工人(线程)都可以使用这些设备,每个线程都可以使用这些共享内存。

5、有的设备只能让1个人使用,有人使用的时候,其他人就不能再使用了。这代表一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。

6、一个防止其他工人使用的简单方法,就是设备加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫"互斥锁"(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域。

7、还有的设备可以同时让n个人使用,也就是说,如果人数大于n,多出来的人只能在外面等着。这好比某些内存区域,只能供给固定数目的线程使用。

8、这时的解决方法,就是在门口挂n把钥匙。进去的人就取一把钥匙,出来时再把钥匙挂回原处。后到的人发现钥匙没有了,就知道必须在门口排队等着了。这种做法叫做"信号量"(Semaphore),用来保证多个线程不会互相冲突。不难看出,mutex是semaphore的一种特殊情况(n=1时)。也就是说,完全可以用后者替代前者。但是,因为mutex较为简单,且效率高,所以在必须保证资源独占的情况下还是采用这种设计。

操作系统的设计,因此可以归结为三点:

       ①以多进程形式,允许多个任务同时运行;

       ②以多线程形式,允许单个任务分成不同的单元部分运行;

       ③提供协调机制,一方面防止进程之间、线程之间产生冲突,另一方面允许进程之间、线程之间共享资源。

     1、特性:

       轻量级:线程基本上不拥有系统资源,只拥有一点必不可少的、能保证独立运行的资源。故线程的切换非常迅速且开销小(在同一进程中的)。

       并发性:线程都可以同其他线程一起并发执行,不管是同进程中的线程还是不同进程中的线程;

       共享进程资源:在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量等。由于同一个进程内的线程共享内存和文件,所以线程之间互相通信不必调用内核。

     2、状态

       ①使用继承Thread类或者实现Runnable/Callable接口的方式创建线程后,此时创建的线程就是新建状态

       ②当新建状态的线程调用start()方法后就处于就绪状态(即“可运行状态”);

       ③当线程获取到CPU资源后就进入运行状态,在此之后又分5种情况:

              ❶当运行状态的线程的run()方法或main()方法执行结束后,线程就处于终止状态

              ❷当运行状态的线程调用自身的yield()方法后,意味着主动放弃CPU资源,重新回到就绪状态,与其他就绪状态的线程公平竞争获取CUP资源,系统有可能又把CPU资源再一次分配给该线程;

              ❸当运行状态的线程调用自身的sleep()方法或其他线程调用join()方法后当前线程就处于阻塞状态(不释放锁),直到sleep()或join()方法结束后阻塞的线程才自动进入就绪状态,等待时机获取CPU资源继续执行本线程;

              ❹当线程刚进入可运行状态,如果发现将要调用的资源被synchronized(同步),获取不到锁标记,将会立即进入锁池状态,等待获取锁标记。这时的锁池里也许已经有了其他线程在等待获取锁标记,这时它们处于锁池队列(先到先得),一旦线程获得锁标记后,才转为可运行状态,等待系统分配CPU时间片;

              ❺当运行状态的线程调用obj的wait()方法后会进入阻塞状态(会释放锁)进入这个状态后是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒(由于notify()只能唤醒一个线程,但我们又不能确定具体唤醒的是哪一个线程,也许我们需要唤醒的线程不能够被唤醒,因此在实际使用中,一般都用notifyAll()方法,唤醒所有线程),线程被唤醒后会进入锁池队列,等待获取锁标记;

     3、使用:

       当 Java 虚拟机启动时会有某用户线程调用某个指定类的 main 方法,这个线程称为主线程,只有在需要多线程的时候才需要创建线程。获取当前运行的线程的方法Thread.currentThread()。

       1>线程的分类:

        ①用户线程(User Thread):

        又称前台线程,当有用户线程运行时,JVM不能关闭。当没有用户线程运行时,不管有没有守护线程,JVM都会自动关闭。 默认情况下创建的线程都是用户线程。

        ②守护线程(Daemon Thread):

        又称后台线程,守护线程的作用是为其他线程的运行提供便利服务的,比如垃圾回收线程就是一个很称职的守护者。守护线程一般是由操作系统创建,当然也可以由用户自己创建。在创建完线程之后,在调用start()方法前调用setDaemon(true)方法将该线程设置为守护线程,调用isDaemon()可以判断该线程是否是守护线程。

       2>线程的命名:

        一个运行中的线程总是有名字的,名字有两个来源:一种是虚拟机默认给的名字,另一种是用户自定义设置的名字。线程都可以调用setName()方法设置线程名字,也可以调用getName()方法获取线程的名字,连主线程也不例外。在没有指定线程名字的情况下,虚拟机总会为线程指定名字。并且主线程的名字总是main,非主线程的名字不确定。

       3>线程的优先级:

        每个线程都有一个优先级,高优先级线程的执行优先于低优先级线程,优先级高的线程会获得较多的运行机会。当设计多线程应用程序的时候,一定不要依赖于线程的优先级。因为线程调度优先级操作是没有保障的,只能把线程优先级作用作为一种提高程序效率的方法,但是要保证程序不依赖这种优先级规则。

        Java线程的优先级用整数表示,取值范围是1~10,Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。每个线程都有默认的优先级,主线程的默认优先级为Thread.NORM_PRIORITY。Thread类有以下三个静态常量:

        ①static int MAX_PRIORITY:线程可以具有的最高优先级,取值为10;

        ②static int MIN_PRIORITY:线程可以具有的最低优先级,取值为1;

        ③static int NORM_PRIORITY: 分配给线程的默认优先级,取值为5。

       4>创建方式:

        ①继承Thread类

class MyThread extends Thread {
    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        int count = 0;
        while (count < 10) {
            System.out.println(threadName + " 的run()方法中第 " + count + " 次循环输出");
            count++;
        }
    }
}

public class Test {
    public static void main(String[] args) {
        Thread thread1 = new MyThread();
        thread1.setName("线程 1");
        thread1.start();

        Thread thread2 = new MyThread();
        thread2.setName("线程 2");
        thread2.start();
    }
}

        ②实现Runnable接口

        当使用Runnable接口时,不能直接创建该类的对象并启动它,必须用该类的对象创建一个Thread类的对象后启动它。

class MyRunnable implements Runnable {
    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        int count = 0;
        while (count < 10) {
            System.out.println(threadName + " 的run()方法中第 " + count + " 次循环输出");
            count++;
        }
    }
}

public class Test {
    public static void main(String[] args) {
        Runnable runnable = new MyRunnable();

        Thread thread1 = new Thread(runnable);
        thread1.setName("线程 1");
        thread1.start();

        Thread thread2 = new Thread(runnable);
        thread2.setName("线程 2");
        thread2.start();
    }
}

        ③实现Callable接口

        Callable接口类似于Runnable,但是Runnable不会返回结果,而Callable功能更强大一些,被线程执行后可以返回值,这个返回值可以被接口Future拿到。

        FutureTask实现了Runnable和Future接口,所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值。

class MyCallable implements Callable<String> {
    @Override
    public String call() {
        String threadName = Thread.currentThread().getName();
        int count = 0;
        while (count < 10) {
            System.out.println(threadName + " 的call()方法中第 " + count + " 次循环输出");
            count++;
        }
        return threadName + " 执行完毕";
    }
}

public class Test {
    public static void main(String[] args) {
        Callable<String> callable1 = new MyCallable();
        FutureTask<String> futureTask1 = new FutureTask<>(callable1);
        Thread thread1 = new Thread(futureTask1);
        thread1.setName("线程 1");

        Callable<String> callable2 = new MyCallable();
        FutureTask<String> futureTask2 = new FutureTask<>(callable2);
        Thread thread2 = new Thread(futureTask2);
        thread2.setName("线程 2");

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

        try {
            String result1 = futureTask1.get();
            System.out.println(result1);

            String result2 = futureTask2.get();
            System.out.println(result2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

        ④使用线程池

        线程池的原理是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。当有任务时,从池中取一个线程执行该任务,任务执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源。

        ❶Executors创建线程池

        java.util.concurrent.Executors类中提供了很多创建线程池的方法,常用方法有:newFixedThreadPool、newCachedThreadPool、newScheduledThreadPool、newSingleThreadExecutor、newSingleThreadScheduledExecutor等。execute方法只能往线程池中添加Runnable线程,而Callable线程和Runnable线程都可以用submit方法来被添加到线程池中。

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

class MyThread extends Thread {
    private String name;

    public MyThread(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        Thread.currentThread().setName(name);
        String threadName = Thread.currentThread().getName();
        System.out.println(threadName + " 正在执行");
    }
}

public class Test {
    public static void main(String[] args) {
        // 创建固定大小的线程池
        ExecutorService executorService1 = Executors.newFixedThreadPool(2);
        // 创建可变大小的线程池
        ExecutorService executorService2 = Executors.newCachedThreadPool();
        // 创建有延时的线程池
        ScheduledExecutorService scheduledExecutorService3 = Executors.newScheduledThreadPool(2);
        // 创建单任务的线程池
        ExecutorService executorService4 = Executors.newSingleThreadExecutor();
        // 创建单任务有延时的线程池
        ScheduledExecutorService scheduledExecutorService5 = Executors.newSingleThreadScheduledExecutor();

        Thread thread1 = new MyThread("线程1");
        Thread thread2 = new MyThread("线程2");
        Thread thread3 = new MyThread("线程3");
        Thread thread4 = new MyThread("线程4");
        Thread thread5 = new MyThread("线程5");

        executorService1.execute(thread1);
        executorService2.execute(thread2);
        // 往有延时的线程池中添加线程
        scheduledExecutorService3.schedule(thread3, 5, TimeUnit.SECONDS);
        executorService4.execute(thread4);
        scheduledExecutorService5.schedule(thread5,5, TimeUnit.SECONDS);

        executorService1.shutdown();
        executorService2.shutdown();
        scheduledExecutorService3.shutdown();
        executorService4.shutdown();
        scheduledExecutorService5.shutdown();
    }
}

        但阿里代码规范不推荐此种创建方式,原因如下:

        1>FixedThreadPool 和 SingleThreadExecutor:

        底层方法的blockingQueue队列长度为Integer.MAX_VALUE,可能会因堆积大量的请求而导致OOM;

        2>CachedThreadPool、ScheduledThreadPool 和 SingleThreadScheduledExecutor:

        底层方法的maximumPoolSize线程数量为Integer.MAX_VALUE,可能会因创建大量的线程而导致OOM。

因此推荐使用底层的java.util.concurrent.ThreadPoolExecutor创建线程池。

        ❷ThreadPoolExecutor创建线程池

        java.util.concurrent.Executors类中提供的创建线程池的newXxxThreadPool方法,底层实际上都是调用了java.util.concurrent.ThreadPoolExecutor。为避免可能出现OOM,一般都使用ThreadPoolExecutor创建线程池。

public ThreadPoolExecutor(int corePoolSize,      // 池中所保存的核心线程数,包括空闲线程
                          int maximumPoolSize,   // 池中允许的最大线程数
                          long keepAliveTime,    // 空闲的非核心线程等待新任务的最长时间,如果时间到还无任务则线程销毁
                          TimeUnit unit,         // 时间参数的时间单位
                          BlockingQueue<Runnable> workQueue, // 阻塞队列,用于存放等待核心线程处理的任务
                          ThreadFactory threadFactory,       // 创建新线程时使用的工厂
                          RejectedExecutionHandler handler)  // 超出池中线程数量和队列容量而使任务被阻塞时对这些阻塞任务的处理方式

        核心线程数corePoolSize至少是0,如果池中核心线程数未达到corePoolSize,那么即使池中已有的核心线程处于空闲状态也还是会继续创建新的核心线程来处理新任务。

        最大线程数maximumPoolSize必须大于0,且必须大于等于corePoolSize,否则抛IllegalArgumentException。

        非核心线程(maximumPoolSize - corePoolSize 的那些线程)在keepAliveTime时间之后如果没有新任务要处理就会销毁,但核心线程在创建后则会一直存活下去。

        workQueue、threadFactory和handler都不能为null,否则抛NullPointerException。handler有内置的4种策略:CallerRunsPolicy、AbortPolicy、DiscardPolicy和DiscardOldestPolicy。

使用示例如下:

class MyThread extends Thread {
    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        System.out.println(threadName + " 正在执行");
    }
}

public class Test {
    public static void main(String[] args) {
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(1);
        ThreadFactory threadFactory = new ThreadFactoryBuilder().setNamePrefix("pool1-").build();
        RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();
        ExecutorService executorService =
                new ThreadPoolExecutor(1, 2, 1, TimeUnit.SECONDS, workQueue, threadFactory, handler);
        Thread thread1 = new MyThread();
        Thread thread2 = new MyThread();
        Thread thread3 = new MyThread();
        Thread thread4 = new MyThread();

        executorService.execute(thread1);
        executorService.execute(thread2);
        executorService.execute(thread3);
        executorService.execute(thread4);

        executorService.shutdown();
    }
}

       5>常用方法:

        ①public void start()

        可以让新建线程变为可运行状态,准备执行线程中的run()方法。

        ②public static void yield()

        暂停当前正在执行的线程对象,并执行其他线程。作用是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。但实际中无法保证yield()达到让步目的,因为让步的线程有可能再次获得CPU。

        ③public static void sleep(long millis,int nanos)

        在指定的毫秒数加指定的纳秒数内让当前正在执行的线程休眠(暂停执行),线程休眠时,它所持的锁都不会释放。

        ❶线程休眠是帮助所有线程获得运行机会的最好方法;

        ❷线程休眠到期自动苏醒,并返回到可运行状态,不是运行状态。sleep()中指定的时间是线程不会运行的最短时间,因此sleep()方法不能保证该线程睡眠到期后就开始执行;

        ④public final void join(long millis,int nanos)

        当前执行的线程A等待指定的毫秒数加指定的纳秒数,而调用join方法的线程B在此期间执行指定的毫秒数加指定的纳秒数,之后等待的线程A再变为可运行状态。作用就是将几个并行的线程合并为一个单线程执行,应用场景是当一个线程必须等待另一个线程执行完毕才能执行。

        ⑤public final void wait(long millis,int nanos)

        使已获取到对象锁的线程释放对象锁,并且这个线程进入等待队列等待指定的时间。如果参数不指定时间,则一直等待,直到被notify()或notifyAll()唤醒。

        ⑥public final native void notify()

        唤醒被wait()放入等待队列中的一个线程,被唤醒的线程进入锁池队列。如果等待队列中有多个线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意性的;

        ⑦public final native void notifyAll()

        唤醒被wait()放入等待队列中的所有线程,被唤醒的线程都进入锁池队列中进行竞争。

、线程同步和锁:

        要让并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

       如果多个线程同时访问并修改同一个对象以及它的成员变量时,由于多个线程同时都在修改数据,会造成数据不正确。比如线程A多次充钱,线程B多次取钱:

class BankAccount {
    private long amount;
    public boolean deposit(long amount) {
        if (amount <= 0) {
            System.out.println("金额输入不正确");
            return false;
        }
        this.amount = this.amount + amount;
        System.out.println("存入: " + amount);
        return true;
    }

    public boolean withdraw(long amount) {
        if (amount <= 0) {
            System.out.println("金额输入不正确");
            return false;
        }
        if (amount > this.amount) {
            System.out.println("余额不足");
            return false;
        }
        this.amount = this.amount - amount;
        System.out.println("取出: " + amount);
        return true;
    }
}
class Parent extends Thread {
    private BankAccount bankAccount;

    public Parent(BankAccount bankAccount) {
        this.bankAccount = bankAccount;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 12; i++) {
            this.bankAccount.deposit(1000L);
        }
    }
}

class Child extends Thread {
    private BankAccount bankAccount;

    public Child(BankAccount bankAccount) {
        this.bankAccount = bankAccount;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 12; i++) {
            this.bankAccount.withdraw(1000L);
        }
    }
}

public class Test {
    public static void main(String[] args) {
        BankAccount bankAccount = new BankAccount();

        Child child = new Child(bankAccount);
        Parent parent = new Parent(bankAccount);

        child.start();
        parent.start();
    }
}

       多执行几次可以看到,即使没存够钱也能取到钱,原因是两个线程不加控制地访问bankAccount对象并修改其数据。如果要保证结果的正确合理,那就需要对bankAccount对象或amount的访问加以限制,每次只能有一个线程在访问,这样就能保证bankAccount对象中amount的合理性了。 

        线程同步的方式有:

     1、阻塞队列:

        一个指定长度的队列,如果队列满了,put()添加新元素的操作会被阻塞等待,直到有空位为止。同样,当队列为空时候,take()获取队列元素的操作同样会阻塞等待,直到有可用元素为止。

       1>阻塞队列: 

        java.util.concurrent.BlockingQueue:能将元素添加到队列尾部,能从队列头部取出元素,先进先出。

public class Test {
    private static final int BLOCKING_QUEUE_SIZE = 5;
    private static final BlockingQueue<Integer> BLOCKING_QUEUE = new ArrayBlockingQueue<>(BLOCKING_QUEUE_SIZE);

    public static class MyRunnable implements Runnable {
        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(5000);
                    if (BLOCKING_QUEUE.size() == BLOCKING_QUEUE_SIZE) {
                        System.out.println("成功删除元素:" + BLOCKING_QUEUE.take());
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.setDaemon(true);
        thread.start();

        try {
            for (int i = 0; i < 10; i++) {
                System.out.println("即将向队列中添加元素:" + i);
                BLOCKING_QUEUE.put(i);
                System.out.println("成功添加元素:" + i);
                System.out.println("**************************************************");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("程序运行结束");
    }
}

       2>双端阻塞队列:

        java.util.concurrent.BlockingDeque:两端都可以进出。

public class Test {
    private final static int BLOCKING_DEQUE_SIZE = 5;
    private final static BlockingDeque<Integer> BLOCKING_DEQUE = new LinkedBlockingDeque<>(BLOCKING_DEQUE_SIZE);

    public static class MyRunnable implements Runnable {
        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(5000);
                    Integer first = BLOCKING_DEQUE.getFirst();
                    Integer last = BLOCKING_DEQUE.getLast();
                    Integer i;
                    if (first >= last) {
                        i = BLOCKING_DEQUE.takeFirst();
                    } else {
                        i = BLOCKING_DEQUE.takeLast();
                    }
                    System.out.println("成功删除元素i:" + i + ", " + BLOCKING_DEQUE);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.setDaemon(true);
        thread.start();

        try {
            for (int i = 0; i < 20; i++) {
                if (i % 2 == 0) {
                    System.out.println("即将向队列头部添加元素:" + i);
                    BLOCKING_DEQUE.putFirst(i);
                } else {
                    System.out.println("即将向队列尾部添加元素:" + i);
                    BLOCKING_DEQUE.putLast(i);
                }
                System.out.println("成功添加元素i:" + i + ", " + BLOCKING_DEQUE);
                System.out.println("**************************************************");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("程序运行结束");
    }
}

     2、信号量(Semaphore):

        也称为信号灯,负责协调各个线程正确、合理使用公共资源。它是一个非负整数,所有获取到它的线程都会将该整数减一,当该整数值为零时,所有试图获取它的线程都将处于阻塞等待状态。信号量上有两种操作: Request(请求)和 Release(释放)。当一个线程调用Request操作时,要么获取到信号量然后将信号量减一,要么一直等下去,直到信号量大于1或超时。Release实际上是在信号量上执行加操作,是因为加操作实际上是释放了由线程占用的信号量资源。

        信号量分为单值和多值两种,前者只能被一个线程获得,后者可以被若干个线程获得。单值Semaphore对象可以实现互斥锁的功能,并且可以是由一个线程加“锁”,再由另一个线程释放“锁”,这可应用于死锁恢复的一些场合。另外信号量可以设置是否采用公平模式,如果采用公平模式,那么线程将会按到达的顺序(FIFO)获取信号量,如果是非公平模式,那么后到的线程有可能先获取到信号量。比如只有5个车位的停车场:

public class Test {
    // 只能5个车位
    private static final Semaphore SEMAPHORE = new Semaphore(5, true);

    public static class MyRunnable implements Runnable {
        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(5000);
                    // 如果没有空车位则释放
                    if (SEMAPHORE.availablePermits() == 0) {
                        SEMAPHORE.release();
                        System.out.println("车位释放成功");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.setDaemon(true);
        thread.start();

        // 10辆车要停车
        for (int i = 0; i < 10; i++) {
            try {
                // 获取令牌
                System.out.println("第" + i + "位准备进入");
                SEMAPHORE.acquire();
                System.out.println("进入成功");
                System.out.println("可用信号量为:" + SEMAPHORE.availablePermits());
                System.out.println("**************************************************");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("程序运行结束");
    }
}

     3、使用synchronized关键字修饰方法或块

        Java中每个对象都有一个内置锁,一个对象只有一个锁。如果线程A获得该对象的锁,其他线程就都不能访问该对象,直到线程A把这个锁释放。获得一个对象的锁也称为获取锁、锁定对象、在对象上加锁或在对象上同步。

        当线程运行到实例对象的synchronized(同步)方法或块上时,自动获得当前实例对象的内置锁,其他运行到此对象的synchronized方法或块上的线程必须在锁池队列中等待。这些等待的线程不能访问当前实例对象的所有synchronized方法和块,但能访问当前实例对象的非synchronized方法和块。当线程退出synchronized方法或块才释放内置锁,锁池队列中等待的线程才能获取到这个内置锁然后变为可运行状态。

        同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,而是同步关键代码块即可。以刚才线程A多次充钱,线程B多次取钱为例,只需要做如下修改:

class BankAccount {
    private long amount;
    public synchronized boolean deposit(long amount) {
        if (amount <= 0) {
            System.out.println("金额输入不正确");
            return false;
        }
        this.amount = this.amount + amount;
        System.out.println("存入: " + amount);
        return true;
    }

    public boolean withdraw(long amount) {
        synchronized (this){
            if (amount <= 0) {
                System.out.println("金额输入不正确");
                return false;
            }
            if (amount > this.amount) {
                System.out.println("余额不足");
                return false;
            }
            this.amount = this.amount - amount;
            System.out.println("取出: " + amount);
            return true;
        }
    }
}
class Parent extends Thread {
    private BankAccount bankAccount;

    public Parent(BankAccount bankAccount) {
        this.bankAccount = bankAccount;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 12; i++) {
            this.bankAccount.deposit(1000L);
        }
    }
}

class Child extends Thread {
    private BankAccount bankAccount;

    public Child(BankAccount bankAccount) {
        this.bankAccount = bankAccount;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 12; i++) {
            this.bankAccount.withdraw(1000L);
        }
    }
}

public class Test {
    public static void main(String[] args) {
        BankAccount bankAccount = new BankAccount();

        Child child = new Child(bankAccount);
        Parent parent = new Parent(bankAccount);

        child.start();
        parent.start();
    }
}

        使用synchronized时需要注意:

        ①要明确在哪个对象上同步。在使用同步代码块时,应指定在哪个对象上同步,也就是说要指定获取哪个对象的锁。比如在BankAccount类中对withdraw方法同步时,被加锁的对象就是BankAccount类的实例自己。所以以下方式均可:

// 方式一
public synchronized void withdraw(long amount) {
    this.amount = this.amount - amount;
}

// 方式二
public void withdraw(long amount) {
    synchronized(this){
        this.amount = this.amount - amount;
    }
}

        ②同步静态方法时,需要对整个类对象同步,以下方式均可:

// 方式一
public static synchronized void withdraw(long amount) {
    this.amount = this.amount - amount;
}

// 方式二
public void withdraw(long amount) {
    synchronized(BankAccount.class){
        this.amount = this.amount - amount;
    }
}

        ③synchronized只能修饰方法或块,一个类中可以同时拥有同步和非同步方法;

        ④一个线程可以获得多个对象的锁:比如在一个对象的同步方法里面调用另外一个对象的同步方法,则获取了两个对象的锁;

        ⑤如果两个线程要执行类中(不同或相同)的synchronized方法,并且两个线程使用相同的实例来调用方法,那么同时只能有一个线程能够执行任意同步方法,另一个需要等待,直到锁被释放。也就是说:如果一个线程在对象上获得锁,任何其他线程就不能再获得该对象的锁,也就不能执行对象对应的类中定义的任何同步方法。

        ⑥静态同步方法和非静态同步方法永远不会彼此阻塞,因为静态方法锁定在Class对象上,非静态方法锁定在该类的对象上。

        ⑦当一个类已经通过同步等方式保护它的数据时,这个类就称为“线程安全的”。即使是线程安全的类,也应该特别小心,因为操作的线程之间仍然不一定安全。

        ⑧在使用synchronized关键字时,应尽量避免在同步方法或同步块中使用sleep方法,因为同步块占着对象锁,sleep方法又不会释放CPU资源,不但严重影响效率,也不合逻辑。

        ⑨在同步块内调用yeild方法让出CPU资源也没有意义,因为yeild不会让当前线程释放锁。其他互斥线程还是无法访问同步程序块,当然与同步程序块无关的线程可以获得更多的执行时间。

        ⑩一般结合wait()方法使用:当在对象上调用wait()方法时,执行该代码的线程立即释放它在对象上的锁并释放CPU资源。但调用notify()或notifyAll()时,并不意味着当前线程会释放其锁,因此调用notify()并不意味着对象锁变得可用。

     4、使用volatile关键字修饰变量

        Java内存模型(JMM)规定:所有的变量都是存在主存中(类似物理内存),每个线程都有自己的工作内存(类似高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作,并且每个线程不能访问其他线程的工作内存。但JMM允许编译器和处理器对指令进行重排序,重排序不会影响单线程程序执行的最终结果,却会影响多线程并发执行的正确性。

        volatile可以用在除final修饰外的任何变量前面,因为final型的变量是禁止修改的。当一个共享变量被volatile修饰时,它会保证变量修改后的值会立即被更新到主存,当有其他线程需要读取此变量时,它会通过内存从主存中重新读取新值,并且此变量禁止进行指令重排。因此volatile关键字可以用来保证可见性和有序性,但不保证原子性,无法完全替代synchronized关键字,所以满足如下条件才能使用volatile进行线程同步:

        1>对变量的写操作不依赖于当前值;

        2>该变量没有包含在具有其他变量的不变式中;

     5、使用Atomic原子类型

        原子类型虽然可以保证原子性,但无法保证可见性和有序性,所以通常还应该使用锁等同步机制来控制整个程序的安全性。

定义原子类型变量的方法形如下:

private AtomicLong aLong = new AtomicLong(10000);        

     6、使用java.util.concurrent.locks包中类

       1>java.util.concurrent.locks.ReentrantLock类:

        Lock实现提供比使用synchronized方法和语句可以获得的更广泛的锁定操作,它们允许更灵活的结构化,可能具有完全不同的属性,并且可以支持多个相关联的对象Condition。实现类java.util.concurrent.locks.ReentrantLock不区分读写,称为“普通锁”。以刚才线程A多次充钱,线程B多次取钱为例,需要做如下修改:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class BankAccount {
    private long amount;
    public boolean deposit(long amount) {
        if (amount <= 0) {
            System.out.println("金额输入不正确");
            return false;
        }
        this.amount = this.amount + amount;
        System.out.println("存入: " + amount);
        return true;
    }
    public boolean withdraw(long amount) {
        if (amount <= 0) {
            System.out.println("金额输入不正确");
            return false;
        }
        if (amount > this.amount) {
            System.out.println("余额不足");
            return false;
        }
        this.amount = this.amount - amount;
        System.out.println("取出: " + amount);
        return true;
    }
}
class Parent extends Thread {
    private BankAccount bankAccount;
    private Lock lock;

    public Parent(BankAccount bankAccount, Lock lock) {
        this.bankAccount = bankAccount;
        this.lock = lock;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 12; i++) {
            this.lock.lock();
            this.bankAccount.deposit(1000L);
            this.lock.unlock();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

class Child extends Thread {
    private BankAccount bankAccount;
    private Lock lock;

    public Child(BankAccount bankAccount,Lock lock) {
        this.bankAccount = bankAccount;
        this.lock = lock;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 12; i++) {
            this.lock.lock();
            this.bankAccount.withdraw(1000L);
            this.lock.unlock();
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

public class Test {
    public static void main(String[] args) {
        BankAccount bankAccount = new BankAccount();
        Lock lock = new ReentrantLock();

        Child child = new Child(bankAccount, lock);
        Parent parent = new Parent(bankAccount, lock);

        child.start();
        parent.start();
    }
}

       2>java.util.concurrent.locks.ReentrantReadWriteLock类:

        在“普通锁”的基础上为了提高性能,又提供了“读写锁”,在读的地方使用读锁,在写的地方使用写锁。读写锁的特点是:读读不互斥、读写互斥、写写互斥。

       3>java.util.concurrent.locks.Condition接口:

        条件变量将 Object 的方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用。条件变量的实例化是通过一个Lock对象上调用newCondition()方法来获取的,这样条件就和一个锁对象绑定起来了。因此Java中的条件变量只能和锁配合使用,来控制并发程序访问竞争资源的安全。

        条件变量常用方法

        1)void await():使当前线程处于等待状态直到被唤醒。

        2)void signal():唤醒一个等待线程,如果所有的线程都在等待此条件,则选择其中的一个唤醒。

        3)void signalAll():唤醒所有等待线程,如果所有的线程都在等待此条件,则唤醒所有线程。

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class BankAccount {
    private long amount;
    private Lock lock = new ReentrantLock();
    private Condition depositCondition = lock.newCondition();
    private Condition withdrawCondition = lock.newCondition();
    public boolean deposit(long amount) {
        lock.lock();
        if (amount <= 0) {
            System.out.println("金额输入不正确");
            lock.unlock();
            return false;
        }
        this.amount = this.amount + amount;
        System.out.println("存入: " + amount);
        withdrawCondition.signalAll();
        lock.unlock();
        return true;
    }

    public boolean withdraw(long amount) {
        lock.lock();
        if (amount <= 0) {
            System.out.println("金额输入不正确");
            lock.unlock();
            return false;
        }
        if (amount > this.amount) {
            System.out.println("取"+amount+",余额不足");
            try {
                depositCondition.signalAll();
                withdrawCondition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
            return false;
        }
        this.amount = this.amount - amount;
        System.out.println("取出: " + amount);
        depositCondition.signalAll();
        lock.unlock();
        return true;
    }
}
class Parent extends Thread {
    private BankAccount bankAccount;
    private long amount;

    public Parent(BankAccount bankAccount, long amount) {
        this.bankAccount = bankAccount;
        this.amount = amount;
    }

    @Override
    public void run() {
        bankAccount.deposit(amount);
    }
}

class Child extends Thread {
    private BankAccount bankAccount;
    private long amount;

    public Child(BankAccount bankAccount, long amount) {
        this.bankAccount = bankAccount;
        this.amount = amount;
    }

    @Override
    public void run() {
        bankAccount.withdraw(amount);
    }
}

public class Test {
    public static void main(String[] args) {
        BankAccount bankAccount = new BankAccount();

        Child child1 = new Child(bankAccount, 1000);
        Parent parent1 = new Parent(bankAccount, 2000);
        Child child2 = new Child(bankAccount, 3000);
        Parent parent2 = new Parent(bankAccount, 1500);
        Child child3 = new Child(bankAccount, 1000);
        Child child4 = new Child(bankAccount, 1000);

        child1.start();
        parent1.start();
        child2.start();
        parent2.start();
        child3.start();
        child4.start();
    }
}

四、线程死锁

       当两个线程被阻塞,每个线程在等待另一个线程释放CPU资源时就发生死锁。比如:

class Bean{
}

public class Test{
    Bean bean1 = new Bean();
    Bean bean2 = new Bean();
    Thread t1 = new Thread(){
        public void run(){
            synchronized(bean1){
                System.out.println("thread1已锁定bean1");
                synchronized(bean2){
                    System.out.println("thread1已锁定bean2");
                }
            }
        }
    };
    Thread t2 = new Thread(){
        public void run(){
            synchronized(bean2){
                System.out.println("thread2已锁定bean2");
                synchronized(bean1){
                    System.out.println("thread2已锁定bean1");
                }
            }
        }
    };
    public static void main(String[] args) {
        Test test= new Test();
        test.t1.start();
        test.t2.start();
    }
}

Thread1和Thread2的实例在访问bean1和bean2时就有可能发生死锁,发生死锁也是概率发生的。

五、障碍器

        为了适应一种新的设计需求,比如一个大型的任务,常常需要分配好多子任务去执行,只有当所有子任务都执行完成时候,才能执行主任务,这时候就可以选择障碍器了。构造方法如下:

        1、public CyclicBarrier(int parties):创建一个 CyclicBarrier对象,它将在给定数量的参与者(线程)处于等待状态时启动,但它不会在启动 barrier 时执行预定义的操作。

        2、public CyclicBarrier(int parties,Runnable barrierAction):创建一个 CyclicBarrier对象,它将在给定数量的参与者(线程)处于等待状态时启动,并在启动 barrier 时执行给定的屏障操作,该操作由最后一个进入 barrier 的线程执行。 

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

//主任务
class MainTask extends Thread {
    public void run() {
        System.out.println("主任务执行...");
    }
}
//子任务
class SubTask extends Thread {
    private String name;
    private CyclicBarrier cyclicBarrier;
    SubTask(String name, CyclicBarrier cyclicBarrier) {
        this.name = name;
        this.cyclicBarrier = cyclicBarrier;
    }
    public void run() {
        try {
            System.out.println("[子任务" + name + "]执行,并通知障碍器已经完成!");
            cyclicBarrier.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (BrokenBarrierException e1) {
            e1.printStackTrace();
        }
    }
}
public class Test {
    public static void main(String[] args) {
        //创建障碍器,并设置子任务达到障碍点时所要执行的主任务
        CyclicBarrier cyclicBarrier = new CyclicBarrier(6, new MainTask());
        new SubTask("A", cyclicBarrier).start();
        new SubTask("B", cyclicBarrier).start();
        new SubTask("C", cyclicBarrier).start();
        new SubTask("D", cyclicBarrier).start();
        new SubTask("E", cyclicBarrier).start();
        new SubTask("F", cyclicBarrier).start();
    }
} 
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值