Java之多线程

Posted 鹤啸九天-西木

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java之多线程相关的知识,希望对你有一定的参考价值。

一、概念

      1、进程(Porcess):

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

        1)特性: 

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

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

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

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

        2)主要状态:

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

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

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

      2、线程(Thread):

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

辅助理解进程与线程:

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

1、电力(CPU)被分成若干部分分别供各个车间(进程)使用,各车间(进程)之间互不影响;

2、假定该工厂的电表保险丝质量太差,不能让所有车间(进程)同时运转而只能让一个车间(进程)运转。也就是说一个车间(进程)运转时其他车间(进程)必须得歇着,即单个CPU只能运行一个任务,任一时刻CPU总是运行一个进程,其他进程处于非运行状态;

3、车间(进程)中有很有工人(线程),他们协同完成一件任务。

4、车间(进程)的空间(CPU)是工人(线程)共享的,比如许多房间是每个工人(线程)都可以进出的。这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。

5、可是每间房间的大小不同,有些房间最多只能容纳一个人,比如厕所。里面有人的时候,其他人就不能进去了。这代表一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。

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

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

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

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

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

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

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

        1)特性:

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

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

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

        2)状态:

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

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

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

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

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

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

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

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

        进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

二、线程使用

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

1、线程的创建方式:

        1>继承Thread类

class Thread1 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 Test1 
    public static void main(String[] args) 
        Thread t1 = new Thread1();
        t1.setName("线程 1");
        t1.start();

        Thread t2 = new Thread1();
        t2.setName("线程 2");
        t2.start();
    

输出:

线程 1 的run()方法中第 0 次循环输出
线程 2 的run()方法中第 0 次循环输出
线程 1 的run()方法中第 1 次循环输出
线程 2 的run()方法中第 1 次循环输出
线程 1 的run()方法中第 2 次循环输出
线程 2 的run()方法中第 2 次循环输出
线程 2 的run()方法中第 3 次循环输出
线程 2 的run()方法中第 4 次循环输出
线程 2 的run()方法中第 5 次循环输出
线程 1 的run()方法中第 3 次循环输出
线程 2 的run()方法中第 6 次循环输出
线程 1 的run()方法中第 4 次循环输出
线程 1 的run()方法中第 5 次循环输出
线程 1 的run()方法中第 6 次循环输出
线程 2 的run()方法中第 7 次循环输出
线程 1 的run()方法中第 7 次循环输出
线程 2 的run()方法中第 8 次循环输出
线程 2 的run()方法中第 9 次循环输出
线程 1 的run()方法中第 8 次循环输出
线程 1 的run()方法中第 9 次循环输出

        2>实现Runnable接口

                当使用Runnable接口时,不能直接创建所需类的对象并运行它,必须从 Thread 类的一个实例内部运行它。

class Runnable1 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 Test2 
    public static void main(String[] args) 
        Runnable r1 = new Runnable1();

        Thread t1 = new Thread(r1);
        t1.setName("线程 1");
        t1.start();

        Thread t2 = new Thread(r1);
        t2.setName("线程 2");
        t2.start();
    

输出:

线程 1 的run()方法中第 0 次循环输出
线程 2 的run()方法中第 0 次循环输出
线程 1 的run()方法中第 1 次循环输出
线程 2 的run()方法中第 1 次循环输出
线程 1 的run()方法中第 2 次循环输出
线程 1 的run()方法中第 3 次循环输出
线程 1 的run()方法中第 4 次循环输出
线程 1 的run()方法中第 5 次循环输出
线程 1 的run()方法中第 6 次循环输出
线程 2 的run()方法中第 2 次循环输出
线程 2 的run()方法中第 3 次循环输出
线程 1 的run()方法中第 7 次循环输出
线程 1 的run()方法中第 8 次循环输出
线程 1 的run()方法中第 9 次循环输出
线程 2 的run()方法中第 4 次循环输出
线程 2 的run()方法中第 5 次循环输出
线程 2 的run()方法中第 6 次循环输出
线程 2 的run()方法中第 7 次循环输出
线程 2 的run()方法中第 8 次循环输出
线程 2 的run()方法中第 9 次循环输出

        3>实现Callable接口

                Callable接口类似于Runnable,但是Runnable不会返回结果,并且无法抛出返回结果的异常,而Callable功能更强大一些,被线程执行后,可以返回值,这个返回值可以被Future拿到,也就是说,Future可以拿到异步执行任务的返回值。

                FutureTask实现了两个接口,Runnable和Future,所以它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值,那么这个组合的使用有什么好处呢?假设有一个很耗时的返回值需要计算,并且这个返回值不是立刻需要的话,那么就可以使用这个组合,用另一个线程去计算返回值,而当前线程在使用这个返回值之前可以做其它的操作,等到需要这个返回值时,再通过Future得到。

class Callable1 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 Test3 
    public static void main(String[] args) 
        Callable<String> c1 = new Callable1();

        FutureTask<String> futureTask1 = new FutureTask<>(c1);
        Thread t1 = new Thread(futureTask1);
        t1.setName("线程 1");

        FutureTask<String> futureTask2 = new FutureTask<>(c1);
        Thread t2 = new Thread(futureTask2);
        t2.setName("线程 2");

        t1.start();
        t2.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();
        
    

输出:

线程 1 的call()方法中第 0 次循环输出
线程 1 的call()方法中第 1 次循环输出
线程 1 的call()方法中第 2 次循环输出
线程 1 的call()方法中第 3 次循环输出
线程 2 的call()方法中第 0 次循环输出
线程 1 的call()方法中第 4 次循环输出
线程 1 的call()方法中第 5 次循环输出
线程 1 的call()方法中第 6 次循环输出
线程 1 的call()方法中第 7 次循环输出
线程 1 的call()方法中第 8 次循环输出
线程 1 的call()方法中第 9 次循环输出
线程 2 的call()方法中第 1 次循环输出
线程 2 的call()方法中第 2 次循环输出
线程 2 的call()方法中第 3 次循环输出
线程 1执行完毕
线程 2 的call()方法中第 4 次循环输出
线程 2 的call()方法中第 5 次循环输出
线程 2 的call()方法中第 6 次循环输出
线程 2 的call()方法中第 7 次循环输出
线程 2 的call()方法中第 8 次循环输出
线程 2 的call()方法中第 9 次循环输出
线程 2执行完毕

2、线程的命名:

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

3、线程的分类:

        1>用户线程(User Thread):

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

        2>守护线程(Daemon Thread):

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

4、线程的优先级:

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

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

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

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

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

5、线程的常用方法:

        1>public void start()

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

        2>public static void sleep(long millis)

        在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。

        3>public static void sleep(long millis,int nanos)

        在指定的毫秒数加指定的纳秒数内让当前正在执行的线程休眠(暂停执行)。

        :①线程睡眠是帮助所有线程获得运行机会的最好方法。

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

        4>public static void yield()

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

        5>public final void join()
        等待正在运行的线程终止。让一个线程B“加入”到另外一个线程A的尾部。只有在A执行完毕之后B才能工作。意义就是将几个并行线程的线程合并为一个单线程执行,应用场景是当一个线程必须等待另一个线程执行完毕才能执行

        6>public final void join(long millis)

        等待正在运行的线程终止的时间最长为 millis 毫秒,如果超过这个时间,则停止等待,变为可运行状态。意义就是将几个并行线程的线程合并为一个单线程执行,应用场景是当一个线程必须等待另一个线程执行完毕才能执行

        7>public final void join(long millis,int nanos)

        等待正在运行的线程终止的时间最长为 millis 毫秒 +nanos 纳秒,如果超过这个时间,则停止等待,变为可运行状态。意义就是将几个并行线程的线程合并为一个单线程执行,应用场景是当一个线程必须等待另一个线程执行完毕才能执行

6、阻塞队列:

        1>阻塞队列:java.util.concurrent.BlockingQueue接口,一个指定长度的队列,如果队列满了,添加新元素的操作会被阻塞等待,直到有空位为止。同样,当队列为空时候,请求队列元素的操作同样会阻塞等待,直到有可用元素为止。

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class Test  
    public static void main(String[] args) 
        BlockingQueue<Integer> bqueue = new ArrayBlockingQueue<>(5);
        try 
            for (int i = 0; i < 10; i++) 
                System.out.println("即将向阻塞队列中添加元素:" + i);
                // 将指定元素添加到此队列中,如果没有可用空间,将一直阻塞等待
                bqueue.put(i);
                System.out.println("向阻塞队列中添加元素:" + i + " 成功");
            
         catch (InterruptedException e) 
            e.printStackTrace();
        
        System.out.println("程序到此运行结束,即将退出...");
    

输出:

即将向阻塞队列中添加元素:0
向阻塞队列中添加元素:0 成功
即将向阻塞队列中添加元素:1
向阻塞队列中添加元素:1 成功
即将向阻塞队列中添加元素:2
向阻塞队列中添加元素:2 成功
即将向阻塞队列中添加元素:3
向阻塞队列中添加元素:3 成功
即将向阻塞队列中添加元素:4
向阻塞队列中添加元素:4 成功
即将向阻塞队列中添加元素:5

        2>双端阻塞队列:java.util.concurrent.BlockingDeque接口,一个指定长度的队列,两端都可以进出。如果栈满了,添加新元素的操作会被阻塞等待,直到有空位为止。同样,当栈为空时候,请求栈元素的操作同样会阻塞等待,直到有可用元素为止。

import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;

public class Test  
    public static void main(String[] args) 
        BlockingDeque<Integer> bdequeue = new LinkedBlockingDeque<>(5);
        try 
            for (int i = 0; i < 10; i++) 
                if (i % 2 == 0) 
                    System.out.println("即将向双端阻塞队列队首添加元素:" + i);
                    // 将指定元素添加到此队列队首,如果没有可用空间,将一直阻塞等待
                    bdequeue.putFirst(i);
                    System.out.println("向双端阻塞队列队首添加元素:" + i + " 成功");
                 else 
                    System.out.println("即将向双端阻塞队列队尾添加元素:" + i);
                    // 将指定元素添加到此队列队尾,如果没有可用空间,将一直阻塞等待
                    bdequeue.putLast(i);
                    System.out.println("向双端阻塞队列队尾添加元素:" + i + " 成功");
                
            
         catch (InterruptedException e) 
            e.printStackTrace();
        
        System.out.println("程序到此运行结束,即将退出...");
    

输出:

即将向双端阻塞队列队首添加元素:0
向双端阻塞队列队首添加元素:0 成功
即将向双端阻塞队列队尾添加元素:1
向双端阻塞队列队尾添加元素:1 成功
即将向双端阻塞队列队首添加元素:2
向双端阻塞队列队首添加元素:2 成功
即将向双端阻塞队列队尾添加元素:3
向双端阻塞队列队尾添加元素:3 成功
即将向双端阻塞队列队首添加元素:4
向双端阻塞队列队首添加元素:4 成功
即将向双端阻塞队列队尾添加元素:5

三、线程同步与锁

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

import lombok.Data;

@Data
public class BankAccount 
    /**
     * 身份证号/客户号
     */
    private String id;
    /**
     * 姓名
     */
    private String name;
    /**
     * 金额
     */
    private Long amount;

    /**
     * 存款
     *
     * @param amount 存款金额
     */
    public void save(Long amount) 
        this.amount = this.amount + amount;
        System.out.println("打生活费:" + amount + ",打款后账户余额:" + this.amount);
    

    /**
     * 取款
     *
     * @param amount 取款金额
     */
    public void withdraw(Long amount) 
        if (this.amount < amount) 
            System.out.println("取生活费:" + amount + ",取款前账户余额:" + this.amount + ",余额不足");
            return;
        
        this.amount = this.amount - amount;
        System.out.println("取生活费:" + amount + ",取款后账户余额:" + this.amount);
    

public 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.save(1000L);
        
    
public 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();
        bankAccount.setId("000001");
        bankAccount.setName("张三");
        bankAccount.setAmount(0L);

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

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

输出:

打生活费:1000,打款后账户余额:1000
取生活费:1000,取款后账户余额:0
打生活费:1000,打款后账户余额:1000
取生活费:1000,取款后账户余额:0
打生活费:1000,打款后账户余额:1000
取生活费:1000,取款后账户余额:0
打生活费:1000,打款后账户余额:1000
取生活费:1000,取款后账户余额:0
打生活费:1000,打款后账户余额:1000
取生活费:1000,取款后账户余额:0
打生活费:1000,打款后账户余额:1000
取生活费:1000,取款后账户余额:0
打生活费:1000,打款后账户余额:1000
取生活费:1000,取款后账户余额:0
打生活费:1000,打款后账户余额:1000
取生活费:1000,取款后账户余额:0
打生活费:1000,打款后账户余额:1000
取生活费:1000,取款后账户余额:0
打生活费:1000,打款后账户余额:1000
打生活费:1000,打款后账户余额:1000
取生活费:1000,取款后账户余额:0
打生活费:1000,打款后账户余额:2000
取生活费:1000,取款后账户余额:1000
取生活费:1000,取款后账户余额:0

        可以看出输出的最后5条结果是不合理的,连续打款2次后余额仍然是1000。原因是两个线程不加控制的访问Foo对象并修改其数据所致。如果要保证结果的正确合理性,需要达到一个要求,那就是将对Bean实例或num的访问加以限制,每次只能有一个线程在访问。这样就能保证Bean对象中数据的合理性了。

        1、锁的概念:Java中每个对象都有一个内置锁,一个对象只有一个锁(更确切地说应该包括“锁”和“钥匙”)。所以如果A线程获得该对象的锁(更确切地说应该是把该对象加了一把锁,手里拿了钥匙),其他线程就都不能访问这个对象(没有打开该对象上锁的钥匙),直到A线程把这个“锁”撤掉。获得一个对象的锁也称为获取锁、锁定对象、在对象上锁定或在对象上同步。

        2、信号量(Semaphore):有时被称为信号灯,是在多线程环境下使用的一种设施, 它负责协调各个线程, 以保证它们能够正确、合理的使用公共资源。Semaphore分为单值和多值两种,前者只能被一个线程获得,后者可以被若干个线程获得。单个信号量的Semaphore对象可以实现互斥锁的功能,并且可以是由一个线程获得了“锁”,再由另一个线程释放“锁”,这可应用于死锁恢复的一些场合。

               以停车场为例:假设停车场只有三个车位,一开始三个车位都是空的。这时如果同时来了五辆车,看门人允许其中三辆不受阻碍的进入,然后放下车拦,剩下的车则必须在入口等待,此后来的车也都不得不在入口处等待。这时,有一辆车离开停车场,看门人得知后,打开车拦,放入一辆,如果又离开两辆,则又可以放入两辆,如此往复。在这个停车场系统中,车位是公共资源,每辆车好比一个线程,看门人起的就是信号量的作用。

               信号量的特性如下:信号量是一个非负整数(车位数),所有通过它的线程(车辆)都会将该整数减一(通过它当然是为了使用资源),当该整数值为零时,所有试图通过它的线程都将处于等待状态。在信号量上我们定义两种操作: Request(请求)和 Release(释放)。 当一个线程调用Request(请求)操作时,它要么通过然后将信号量减一,要么一直等下去,直到信号量大于1或超时。Release(释放)实际上是在信号量上执行加操作,对应于车辆离开停车场,该操作之所以叫做“释放”是因为加操作实际上是释放了由信号量守护的资源。

               信号量还可以设置是否采用公平模式,如果采用公平模式,则线程将会按到达的顺序(FIFO)执行;如果是非公平模式,则后请求的有可能排在队列的头部。构造方法是:Semaphore(int permits, boolean fair)。使用方法如下:

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

public class Test  
	public static void main(String[] args) 
        // 线程池
        ExecutorService pool = Executors.newCachedThreadPool();
        // 只能5个线程同时访问
        final Semaphore semp = new Semaphore(5);
        // 模拟20个客户端访问
        for (int index = 0; index < 10; index++) 
            final int NO = index;
            Runnable run = new Runnable() 
                public void run() 
                    try 
                        // 获取许可
                        semp.acquire();
                        System.out.println("第" + NO+"位进入");
                        
                        Thread.sleep((long) (Math.random() * 10000));
                        // 访问完后,释放
                        semp.release();
                      //availablePermits()指的是当前信号灯库中有多少个可以被使用
                        System.out.println("可用信号量为:" + semp.availablePermits()); 
                     catch (InterruptedException e) 
                        e.printStackTrace();
                    
                
            ;
            pool.execute(run);
        
        // 退出线程池
        pool.shutdown();
    
 

输出:

第0位进入
第2位进入
第1位进入
第3位进入
第4位进入
第5位进入
可用信号量为:0
可用信号量为:1
第6位进入
可用信号量为:1
第7位进入
可用信号量为:1
第8位进入
可用信号量为:1
第9位进入
可用信号量为:1
可用信号量为:2
可用信号量为:3
可用信号量为:4
可用信号量为:5

        3、同步的四种机制:

                1>使用synchronized关键字修饰方法或块

           当程序运行到非静态的synchronized方法或块上时,自动获得当前实例对象的锁,直到退出synchronized方法或块才释放锁。

        如果线程试图进入同步方法,而其锁已经被占用,则线程在该对象上被阻塞。实质上线程进入该对象的一种池中(锁池队列),必须在那里等待,直到其锁被释放,该线程才能再次变为可运行或运行状态。

        wait()、notify()、notifyAll()都是Object的实例方法。与每个对象具有锁一样,每个对象可以有一个线程列表,这些线程等待来自该对象的信号(通知)。对象调用wait()方法获得这个等待列表。至此该对象不再执行任何该等待队列中线程中的动作,直到调用对象的notify()方法。如果多个线程在同一个对象上等待,则将只选择一个线程(不保证以何种顺序)继续执行。如果没有线程等待,则不采取任何特殊操作。使用wait()、notify()、notifyAll()方法时必须从同步块内调用这些方法:

        1>public final void wait()

        使已获取对象锁的当前线程释放对象锁,重新进入等待队列而成为等待状态;

        2>public final void notify()

        当前线程放弃此对象上的锁定后,才能继续执行被唤醒在此对象上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意性的;

        3>public final void notifyAll()

        当前线程放弃此对象上的锁定后,才能继续执行被唤醒在此对象上等待的所有线程。被唤醒的所有线程将以常规方式在锁池队列中进行竞争。

package test2;
class Bean

class Thread1 extends Thread
	private String name;
	private Bean bean;
	public Thread1(String name,Bean bean)
		this.name=name;
		this.bean=bean;
	
	public void run() 
		this.setName(name);
		synchronized (bean) 
			try 
				System.out.println("线程"+Thread.currentThread().getName()+"获得bean的锁");
				Thread.sleep(3000);
				System.out.println("线程"+Thread.currentThread().getName()+"调用bean的wait()方法");
				Thread.sleep(3000);
				bean.wait();
				System.out.println("线程"+Thread.currentThread().getName()+"即将死亡");
				Thread.sleep(3000);
			 catch (InterruptedException e) 
				e.printStackTrace();
			
		
	

class Thread2 extends Thread
	private String name;
	private Bean bean;
	public Thread2(String name,Bean bean)
		this.name=name;
		this.bean=bean;
	
	public void run() 
		this.setName(name);
		synchronized (bean) 
			try 
				System.out.println("线程"+Thread.currentThread().getName()+"获得bean的锁");
				Thread.sleep(3000);
				System.out.println("线程"+Thread.currentThread().getName()+"调用bean的notify()方法");
				Thread.sleep(3000);
				bean.notify();
				System.out.println("线程"+Thread.currentThread().getName()+"即将死亡");
				Thread.sleep(3000);
			 catch (InterruptedException e) 
				e.printStackTrace();
			
		
	

class Thread3 extends Thread
	private String name;
	private Bean bean;
	public Thread3(String name,Bean bean)
		this.name=name;
		this.bean=bean;
	
	public void run() 
		this.setName(name);
		synchronized (bean) 
			try 
				System.out.println("线程"+Thread.currentThread().getName()+"获得bean的锁");
				Thread.sleep(3000);
				System.out.println("线程"+Thread.currentThread().getName()+"调用bean的notifyAll()方法");
				Thread.sleep(3000);
				bean.notifyAll();
				System.out.println("线程"+Thread.currentThread().getName()+"即将死亡");
				Thread.sleep(3000);
			 catch (InterruptedException e) 
				e.printStackTrace();
			
		
	

public class Test5 
	public static void main(String[] args) 
		Bean bean = new Bean();
		Thread1 thread1 = new Thread1("Thread1",bean);
		Thread2 thread2 = new Thread2("Thread2",bean);
		Thread3 thread3 = new Thread3("Thread3",bean);
		thread1.start();
		thread2.start();
		thread3.start();
	

结果:

线程Thread1获得bean的锁
线程Thread1调用bean的wait()方法
线程Thread3获得bean的锁
线程Thread3调用bean的notifyAll()方法
线程Thread3即将死亡
线程Thread2获得bean的锁
线程Thread2调用bean的notify()方法
线程Thread2即将死亡
线程Thread1即将死亡

可以看出:当在对象上调用wait()方法时,执行该代码的线程立即放弃它在对象上的锁。然而调用notify()时或notifyAll()时,并不意味着当前线程会放弃其锁。因此调用notify()并不意味着对象锁变得可用。

注意:①synchronized只能修饰方法或块;

           ②不必同步类中所有的方法,类可以同时拥有同步和非同步方法;

           ③线程睡眠时,它所持的任何锁都不会释放;

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

           ⑤同步损害并发性,应该尽可能缩小同步范围。同步不但可以同步整个方法,还可以同步方法中一部分代码块;

           ⑥如果两个线程要执行一个类中的synchronized方法,并且两个线程使用相同的实例来调用方法,那么一次只能有一个线程能够执行方法,另一个需要等待,直到锁被释放。也就是说:如果一个线程在对象上获得一个锁,就没有任何其他线程可以进入(该对象的)类中的任何一个同步方法。

                ⑦要明确在哪个对象上同步。在使用同步代码块时候,应该指定在哪个对象上同步,也就是说要指定获取哪个对象的锁。比如:

public synchronized void setNum(int tmp) 
	num = num-tmp;

public void setNum(int tmp) 
	synchronized(this)
		num = num-tmp;
	

效果完全一样。

           ⑧同步静态方法,需要一个用于整个类对象的锁:

public static synchronized void setNum(int tmp) 
	num = num-tmp;

public static void setNum(int tmp) 
	synchronized(Bean.class)
		num = num-tmp;
	

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

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

另外:在使用synchronized关键字时候,应该尽可能避免在synchronized方法或synchronized块中使用sleep方法,因为synchronized程序块占着对象锁,你休息那么其他的线程只能一边等着你醒来执行完了才能执行。不但严重影响效率,也不合逻辑。

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

                2>使用volatile关键字修饰变量

        volatile在高性能的多线程程序中也有很重要的用途,只是这个关键字用不好会出很多问题。

        为什么变量需要volatile来修饰呢?比如做一个i++操作,计算机内部做了三次处理:读取-修改-写入。同样,对于一个long型数据,做了个赋值操作,在32系统下需要经过两步才能完成,先修改低32位,然后修改高32位。假想一下,当将以上的操作放到一个多线程环境下操作时候,有可能出现的问题:如果这些步骤只执行了一部分,而另外一个线程就已经引用了变量值,这样就导致了读取脏数据的问题。通过这个设想,就不难理解volatile关键字了。volatile可以用在任何变量前面,但不能用于final变量前面,因为final型的变量是禁止修改的。也不存在线程安全的问题。

                3>使用Lock接口

        ①java.util.concurrent.locks.Lock接口:实现提供了比使用synchronized 方法和语句可获得的更广泛的锁定操作。此实现允许更灵活的结构,可以具有差别很大的属性,可以支持多个相关的Condition对象。 不区分读写,称为“普通锁”;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//信用卡账户,可随意透支 
class MyCount  
    private String oid;         //账号 
    private int cash;           //账户余额 
    MyCount(String oid, int cash)  
        this.oid = oid; 
        this.cash = cash; 
     
    public String getOid()  
        return oid; 
     
    public void setOid(String oid)  
        this.oid = oid; 
     
    public int getCash()  
        return cash; 
     
    public void setCash(int cash)  
        this.cash = cash; 
     
    public String toString()  
        return "帐户信息:[帐号:"+oid+",余额:"+cash+"]"; 
     

//信用卡的用户 
class User implements Runnable  
    private String name;            //用户名 
    private MyCount myCount;        //所要操作的账户 
    private int iocash;             //操作的金额,当然有正负之分了 
    private Lock myLock;            //执行操作所需的锁对象 
    User(String name, MyCount myCount, int iocash, Lock myLock)  
        this.name = name; 
        this.myCount = myCount; 
        this.iocash = iocash; 
        this.myLock = myLock; 
     
    public void run()  
        //获取锁 
        myLock.lock(); 
        //执行现金业务 
        System.out.println(myCount +","+ name + "正在操作账户,金额为" + iocash); 
        myCount.setCash(myCount.getCash() + iocash); 
        System.out.println(name + "操作账户成功,当前金额为" + myCount.getCash()); 
        //释放锁,否则别的线程没有机会执行了 
        myLock.unlock(); 
     
 
public class Test  
    public static void main(String[] args)  
        //创建并发访问的账户 
        MyCount myCount = new MyCount("0000000000000000", 10000); 
        //创建一个锁对象 
        Lock lock = new ReentrantLock(); 
        //创建一个线程池 
        ExecutorService pool = Executors.newCachedThreadPool(); 
        //创建一些并发访问用户,一个信用卡,存的存,取的取 
        User u1 = new User("张三", myCount, -4000, lock); 
        User u2 = new User("张三他爹", myCount, 6000, lock); 
        User u3 = new User("张三他弟", myCount, -8000, lock); 
        User u4 = new User("张三", myCount, 800, lock); 
        //在线程池中执行各个用户的操作 
        pool.execute(u1); 
        pool.execute(u2); 
        pool.execute(u3); 
        pool.execute(u4); 
        //关闭线程池 
        pool.shutdown(); 
     

输出:

帐户信息:[帐号:0000000000000000,余额:10000],张三正在操作账户,金额为-4000
张三操作账户成功,当前金额为6000
帐户信息:[帐号:0000000000000000,余额:6000],张三正在操作账户,金额为800
张三操作账户成功,当前金额为6800
帐户信息:[帐号:0000000000000000,余额:6800],张三他弟正在操作账户,金额为-8000
张三他弟操作账户成功,当前金额为-1200
帐户信息:[帐号:0000000000000000,余额:-1200],张三他爹正在操作账户,金额为6000
张三他爹操作账户成功,当前金额为4800

        ②java.util.concurrent.locks.ReentrantReadWriteLock类:为了提高性能,Java提供了读写锁,在读的地方使用读锁,在写的地方使用写锁,称为“读写锁”。构造方法中可设置“公平模式”与“非公平模式”。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

//信用卡账户,可随意透支 
class MyCount  
    private String oid;           //账号 
    private int cash;             //账户余额 
    MyCount(String oid, int cash)  
        this.oid = oid; 
        this.cash = cash; 
     
    public String getOid()  
        return oid; 
     
    public void setOid(String oid)  
        this.oid = oid; 
     
    public int getCash()  
        return cash; 
     
    public void setCash(int cash)  
        this.cash = cash; 
     
    public String toString()  
    	return "帐户信息:[帐号:"+oid+",余额:"+cash+"]";
     

//信用卡的用户 
class User implements Runnable  
    private String name;                //用户名 
    private MyCount myCount;            //所要操作的账户 
    private int iocash;                 //操作的金额,当然有正负之分了 
    private ReadWriteLock myLock;       //执行操作所需的锁对象 
    private boolean isQuery;            //是否是查询帐户 
    User(String name, MyCount myCount, int iocash, ReadWriteLock myLock, boolean isQuery)  
        this.name = name; 
        this.myCount = myCount; 
        this.iocash = iocash; 
        this.myLock = myLock; 
        this.isQuery = isQuery; 
     
    public void run()  
        if (isQuery)  
            //获取读锁 
            myLock.readLock().lock(); 
            System.out.println(myCount +","+ name + "正在查询账户余额"); 
            //释放读锁 
            myLock.readLock().unlock(); 
         else  
            //获取写锁 
            myLock.writeLock().lock(); 
            //执行现金业务 
            System.out.println(myCount +","+ name + "正在进行存取操作,存取金额为" + iocash); 
            myCount.setCash(myCount.getCash() + iocash); 
            System.out.println(name + "操作账户成功,当前金额为" + myCount.getCash()); 
            //释放写锁 
            myLock.writeLock().unlock(); 
         
     
 
public class Test  
    public static void main(String[] args)  
        //创建并发访问的账户 
        MyCount myCount = new MyCount("0000000000000000", 10000); 
        //创建一个锁对象 
        ReadWriteLock lock = new ReentrantReadWriteLock(false); 
        //创建一个线程池 
        ExecutorService pool = Executors.newFixedThreadPool(2); 
        //创建一些并发访问用户,一个信用卡,存的存,取的取,好热闹啊 
        User u1 = new User("张三", myCount, -4000, lock, false); 
        User u2 = new User("张三他爹", myCount, 6000, lock, false); 
        User u3 = new User("张三他弟", myCount, -8000, lock, false); 
        User u4 = new User("张三", myCount, 800, lock, false); 
        User u5 = new User("张三他爹", myCount, 0, lock, true); 
        //在线程池中执行各个用户的操作 
        pool.execute(u1); 
        pool.execute(u2); 
        pool.execute(u3); 
        pool.execute(u4); 
        pool.execute(u5); 
        //关闭线程池 
        pool.shutdown(); 
     

输出:

帐户信息:[帐号:0000000000000000,余额:10000],张三正在进行存取操作,存取金额为-4000
张三操作账户成功,当前金额为6000
帐户信息:[帐号:0000000000000000,余额:6000],张三他爹正在进行存取操作,存取金额为6000
张三他爹操作账户成功,当前金额为12000
帐户信息:[帐号:0000000000000000,余额:12000],张三他弟正在进行存取操作,存取金额为-8000
张三他弟操作账户成功,当前金额为4000
帐户信息:[帐号:0000000000000000,余额:4000],张三正在进行存取操作,存取金额为800
张三操作账户成功,当前金额为4800
帐户信息:[帐号:0000000000000000,余额:4800],张三他爹正在查询账户余额

        ③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.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

//普通银行帐户,不可透支
class MyCount  
    private String oid;                               //账号 
    private int cash;                                 //账户余额 
    private Lock lock = new ReentrantLock();          //账户锁 
    private Condition saveCondition = lock.newCondition();    //存款条件 
    private Condition drawCondition = lock.newCondition();    //取款条件 
    MyCount(String oid, int cash)  
        this.oid = oid; 
        this.cash = cash; 
     
    //存款
    public void saving(int x, String name)  
        lock.lock();                          //获取锁 
        if (x > 0)  
            cash += x;                       //存款 
            System.out.println(name + "存款" + x + ",存款之后余额为" + cash); 
         
        drawCondition.signalAll();           //唤醒所有等待线程。 
        lock.unlock();                       //释放锁 
     
    //取款
    public void drawing(int x, String name)  
        lock.lock();                          //获取锁 
        try  
            if (cash - x < 0)  
            	System.out.println(name + "取款" + x + ",但余额只有" + cash +",余额不足"); 
                drawCondition.await();        //余额不足,阻塞取款操作 
             else  
                cash -= x;                    //取款 
                System.out.println(name + "取款" + x + ",取款之后余额为" + cash); 
             
            saveCondition.signalAll();        //唤醒所有存款操作 
         catch (InterruptedException e)  
            e.printStackTrace(); 
         finally  
            lock.unlock();                    //释放锁 
         
     

//存款线程类 
class SaveThread extends Thread  
    private String name;                //操作人 
    private MyCount myCount;            //账户 
    private int x;                      //存款金额 
    SaveThread(String name, MyCount myCount, int x)  
        this.name = name; 
        this.myCount = myCount; 
        this.x = x; 
     
    public void run()  
        myCount.saving(x, name); 
     
 
//取款线程类
class DrawThread extends Thread  
    private String name;             //操作人 
    private MyCount myCount;         //账户 
    private int x;                   //存款金额 

    DrawThread(String name, MyCount myCount, int x)  
        this.name = name; 
        this.myCount = myCount; 
        this.x = x; 
     

    public void run()  
        myCount.drawing(x, name); 
     
 
public class Test  
    public static void main(String[] args)  
        //创建并发访问的账户 
        MyCount myCount = new MyCount("0000000000000000", 10000); 
        //创建一个线程池 
        ExecutorService pool = Executors.newFixedThreadPool(2); 
        Thread t1 = new DrawThread("张三", myCount, 9000); 
        Thread t2 = new SaveThread("李四", myCount, 3600); 
        Thread t3 = new DrawThread("王五", myCount, 8000); 
        Thread t4 = new SaveThread("老张", myCount, 600); 
        Thread t5 = new DrawThread("老牛", myCount, 2000); 
        Thread t6 = new DrawThread("胖子", myCount, 800); 
        //执行各个线程 
        pool.execute(t1); 
        pool.execute(t2); 
        pool.execute(t3); 
        pool.execute(t4); 
        pool.execute(t5); 
        pool.execute(t6); 
        //关闭线程池 
        pool.shutdown(); 
     
 

输出:

张三取款9000,取款之后余额为1000
王五取款8000,但余额只有1000,余额不足
李四存款3600,存款之后余额为4600
老张存款600,存款之后余额为5200
老牛取款2000,取款之后余额为3200
胖子取款800,取款之后余额为2400

           4>使用原子量java.util.concurrent.atomic包中的变量类

以上是关于Java之多线程的主要内容,如果未能解决你的问题,请参考以下文章

2022-08-02 java之多线程

Java之多线程讲解

Java基础之多线程

Java之多线程

Java之多线程

Java基础之多线程