Java多线程

1. 线程、进程和程序

1.1 进程与线程

程序(Program):是一个静态的概念,一般对应于操作系统中的一个可执行的文件
,比如:我们要启动酷狗听音乐,则对应酷狗可执行程序。当我们双击酷狗,则加载
程序到内存中,开始执行该程序,于是产生了"进程".

 进程:执行中的程序叫做进程(Process),是一个动态的概念。现代的操作系统都可以同时启动多个进程。比如:我们在用酷狗听音乐,也可以使用wps写文档,也可以同时用浏览器查看网页。可以通过任务管理器查看当前的进程.

  • 是系统执行资源分配和调度的独立单位

  • 每一进程都有属于自己的存储空间和系统资源

  • 注意:进程A和进程B的内存独立不共享。

 线程:是进程的一个执行路径,一个进程中至少有一个线程,进程中的多个线程共享进程的资源。

单线程:一个进程中包含一个顺序控制流(一条执行路径)

多线程:一个进程中包含多个顺序控制流(多条执行路径)

在java语言中:
 线程A和线程B,堆内存和方法区内存共享。
 但是栈内存独立,一个线程一个栈。

假设启动10个线程,会有10个栈空间,每个栈和每个栈之间,互不干扰,各自执行各自的,这就是多线程并发。

1.2 并发与并行

并发、并行

并发: 多线程操作同一个资源。

  • CPU 只有一核,模拟出来多条线程,那么我们就可以使用CPU快速交替,来模拟多线程。

并行: 多个人(CPU内核)一起行走(运行)

  • CPU多核,多个线程可以同时执行。

 并发编程的本质:充分利用CPU的资源

 2. 线程有几个状态?

线程的五种状态:
        1.新建(new)

                创建了一个新的线程对象

        2.就绪(runnable)

                调用线程的start()方法,处于就绪状态

        3.运行(running)

                获得了CPU时间片,执行程序代码

                就绪状态是进入到运行状态的唯一入口

        4.阻塞(block)

                因为某种原因,线程放弃对CPU的使用权,停止执行,直到进入就绪状态在有可能再次被CPU调度

                阻塞又分为三种:

                1)等待阻塞:运行状态的线程执行wait方法,JVM会把线程放在等待队列中,使本线程进入阻塞状态。

                2)同步阻塞:线程在获得synchronized同步锁失败,JVM会把线程放入锁池中,线程进入同步阻塞。

                3)其他阻塞:调用线程的sleep()或者join()后,线程会进入道阻塞状态,当sleep超时或者join终止或超时,线程重新转入就绪状态

        5.死亡(dead)

                线程run()、main()方法执行结束,或者因为异常退出了run()方法,则该线程结束生命周期,死亡的线程不可再次复生
 

2.1 等待池和锁池

在Java中,每个对象都有两个池,锁(monitor)池和等待池

锁池:假设线程A已经拥有了某个对象(注意:不是类)的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。

等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁),同时线程A就进入到了该对象的等待池中。如果另外的一个线程调用了相同对象的notifyAll()方法,那么处于该对象的等待池中的线程就会全部进入该对象的锁池中,准备争夺锁的拥有权。如果另外的一个线程调用了相同对象的notify()方法,那么仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的锁池.

1. 等待池和锁池都是对象的,就像lock是对象的一样,由对象调用
2. 当锁被线程A占用,线程B和C就只能进入进入锁池,等待锁被A释放后竞争锁;
  当线程A调用Object.wait()之后,线程A释放锁,进入等待池,等待被唤醒;
  当线程A调用Object.wait(1000)之后,线程A释放锁,进入等待池,1秒之后,进入锁池,等待竞争锁;
  线程B和C在锁池中等待竞争锁,如果B得到锁,则线程C继续在锁池中等待竞争锁;
3. 锁池中的线程竞争锁,可以设置优先级获得锁。

3. 创建线程的三种方式

3.1 继承Thread类

1、自定义一个类MyThread类,用来继承与Thread类

2、在MyThread类中重写run()方法

3、在测试类中创建MyThread类的对象

4、启动线程

public class Demo01 {
    public static void main(String[] args) {
        //创建线程
        MyThread t01 = new MyThread();
        MyThread t02 = new MyThread();
        MyThread t03 = new MyThread("线程03");
 
        //开启线程
//        t01.run();
//        t02.run();
//        t03.run();
        // 不会启动线程,不会分配新的分支栈。(这种方式就是单线程。)
        // start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。
        // 这段代码的任务只是为了开启一个新的栈空间,只要新的栈空间开出来,start()方法就结束了。线程就启动成功了。
        // 启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)。
        // run方法在分支栈的栈底部,main方法在主栈的栈底部。run和main是平级的。
        t01.start();
        t02.start();
        t03.start();
        //设置线程名(补救的设置线程名的方式)
        t01.setName("线程01");
        t02.setName("线程02");
        //设置主线程名称
        Thread.currentThread().setName("主线程");
        for (int i = 0; i < 50; i++) {
            //Thread.currentThread() 获取当前正在执行线程的对象
            System.out.println(Thread.currentThread().getName() + ":" + i);
        }
    }
}
class MyThread extends Thread{
    public MyThread() {
    }
 
    public MyThread(String name) {
        super(name);
    }
 
    //run方法是每个线程运行过程中都必须执行的方法
    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            System.out.println(this.getName() + ":" + i);
        }
    }
}

 此处最重要的为start()方法。单纯调用run()方法不会启动线程,不会分配新的分支栈。

start()方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。线程就启动成功了。

启动成功的线程会自动调用run方法(由JVM线程调度机制来运作的),并且run方法在分支栈的栈底部(压栈)。

run方法在分支栈的栈底部,main方法在主栈的栈底部。run和main是平级的。

单纯使用run()方法是不能多线程并发的。

 设置和获取线程名

  • 设置线程名

    • setName(String name):设置线程名

    • 通过带参构造方法设置线程名

  • 获取线程名

    • getName():返回字符串形式的线程名

    • Thread.CurrentThread():返回当前正在执行的线程对象

3.2 实现Runnable接口

1、自定义一个MyRunnable类来实现Runnable接口

2、在MyRunnable类中重写run()方法

3、创建Thread对象,并把MyRunnable对象作为Tread类构造方法的参数传递进去

4、启动线程

public class Demo02 {
    public static void main(String[] args) {
        MyRunnable myRun = new MyRunnable();//将一个任务提取出来,让多个线程共同去执行
        //封装线程对象
        Thread t01 = new Thread(myRun, "线程01");
        Thread t02 = new Thread(myRun, "线程02");
        Thread t03 = new Thread(myRun, "线程03");
        //开启线程
        t01.start();
        t02.start();
        t03.start();
        //通过匿名内部类的方式创建线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 20; i++) {
                    System.out.println(Thread.currentThread().getName() + " - " + i);
                }
            }
        },"线程04").start();
    }
}
//自定义线程类,实现Runnable接口
//这并不是一个线程类,是一个可运行的类,它还不是一个线程。
class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 50; i++) {
            System.out.println(Thread.currentThread().getName() + " - " + i);
        }
    }
}

3.3 实现Callable接口( java.util.concurrent.FutureTask; /JUC包下的,属于java的并发包,老JDK中没有这个包。新特性。)

1、自定义一个MyCallable类来实现Callable接口

2、在MyCallable类中重写call()方法

3、创建FutureTask,Thread对象,并把MyCallable对象作为FutureTask类构造方法的参数传递进去,把FutureTask对象传递给Thread对象。

4、启动线程

        这种方式的优点:可以获取到线程的执行结果。

        这种方式的缺点:效率比较低,在获取t线程执行结果的时候,当前线程受阻塞,效率较低。
 

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class Demo04 {
    public static void main(String[] args) throws Exception {
 
        // 第一步:创建一个“未来任务类”对象。
        // 参数非常重要,需要给一个Callable接口实现类对象。
        FutureTask task = new FutureTask(new Callable() {
            @Override
            public Object call() throws Exception { // call()方法就相当于run方法。只不过这个有返回值
                // 线程执行一个任务,执行之后可能会有一个执行结果
                // 模拟执行
                System.out.println("call method begin");
                Thread.sleep(1000 * 10);
                System.out.println("call method end!");
                int a = 100;
                int b = 200;
                return a + b; //自动装箱(300结果变成Integer)
            }
        });
 
        // 创建线程对象
        Thread t = new Thread(task);
 
        // 启动线程
        t.start();
 
        // 这里是main方法,这是在主线程中。
        // 在主线程中,怎么获取t线程的返回结果?
        // get()方法的执行会导致“当前线程阻塞”
        Object obj = task.get();
        System.out.println("线程执行结果:" + obj);
        // main方法这里的程序要想执行必须等待get()方法的结束
        // 而get()方法可能需要很久。因为get()方法是为了拿另一个线程的执行结果
        // 另一个线程执行是需要时间的。
        System.out.println("hello world!");
    }
}

3.4 Thread 和 Runnable 的区别

  1. Thread 是一个类 ,Runnable 是接口 ,因为在 Java语言里面的继承特性,接口可以支持多继承 ,而类只能单一继承。所以如果在已经存在继承关系的类里面要实现线程的话 ,只能实现 Runnable 接口。

  2. Runnable 表示一个线程的顶级接口,Thread 类其实也是实现了 Runnable 接口。

  3. 站在面向对象的思想来说, Runnable 相当于一个任务 ,而 Thread 才是真正处理 的线程 ,所以我们只需要用 Runnable 去定义一个具体的任务,然后交给 Thread去处理就可以了 ,这样达到了松耦合的设计目的。

  4. Runnable 接口定义了线程执行任务的标准方法 所以,基于这四个点的原因,所以在实际应用中,建议实现 Runnable 接口实现线程的任务定义,然后使用 Thread 的 start ()方法去启动启动线程并执行 Runnable 这个任务。

4. 线程调度

4.1、调整线程优先级

Java线程有优先级,优先级高的线程会获得较多的运行机会。

Java线程的优先级用整数表示,取值范围是1~10,Thread类有以下三个静态常量:

static int MAX_PRIORITY
          线程可以具有的最高优先级,取值为10。
static int MIN_PRIORITY
          线程可以具有的最低优先级,取值为1。
static int NORM_PRIORITY
          分配给线程的默认优先级,取值为5。

 Thread类的setPriority()和getPriority()方法分别用来设置和获取线程的优先级。

每个线程都有默认的优先级。主线程的默认优先级为Thread.NORM_PRIORITY。

4.2、线程睡眠

Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。

4.3、线程等待

Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。 

4.4、线程让步

Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。 

4.5、线程加入

join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。

4.6、线程唤醒

Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。


4.7  wait和sleep区别

1. Thread类的方法:sleep(),yield()等
   Object的方法:wait()和notify()等
2. 每个对象都有一个锁来控制同步访问。Synchronized关键字可以和对象的锁交互,来实现线程的同步。
   sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
3. wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用
所以sleep()和wait()方法的最大区别是:
    sleep()睡眠时,保持对象锁,仍然占有该锁;
    而wait()睡眠时,释放对象锁。
  但是wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException(但不建议使用该方法)。

 sleep()方法
sleep()使当前线程进入停滞状态(阻塞当前线程),让出CUP的使用、目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会;
   sleep()是Thread类的Static(静态)的方法;因此他不能改变对象的机锁,所以当在一个Synchronized块中调用Sleep()方法是,线程虽然休眠了,但是对象的机锁并木有被释放,其他线程无法访问这个对象(即使睡着也持有对象锁)。
  在sleep()休眠时间期满后,该线程不一定会立即执行,这是因为其它线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级。

wait()方法
wait()方法是Object类里的方法;当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时失去(释放)了对象的机锁(暂时失去机锁,wait(long timeout)超时时间到后还需要返还对象锁);其他线程可以访问;
  wait()使用notify或者notifyAlll或者指定睡眠时间来唤醒当前等待池中的线程。
  wiat()必须放在synchronized block中,否则会在program runtime时扔出”java.lang.IllegalMonitorStateException“异常。

 5. 线程同步

为何要使用同步? 
    java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,   从而保证了该变量的唯一性和准确性。

5.1 同步方法

    即有synchronized关键字修饰的方法。 
    由于java的每个对象都有一个内置锁,当用此关键字修饰方法时, 
    内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。 

 public synchronized void save(){}

5.2 同步代码块 

    即有synchronized关键字修饰的语句块。 
    被该关键字修饰的语句块会自动被加上内置锁,从而实现同步 

    代码如: 
    synchronized(object){ }

     注:同步是一种高开销的操作,因此应该尽量减少同步的内容。 
    通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。

 5.3 使用特殊域变量(volatile)实现线程同步

    a.volatile关键字为域变量的访问提供了一种免锁机制, 
    b.使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新, 
    c.因此每次使用该域就要重新计算,而不是使用寄存器中的值 
    d.volatile不会提供任何原子操作,它也不能用来修饰final类型的变量 
 

5.4 使用重入锁实现线程同步

    在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。 
    ReentrantLock类是可重入、互斥、实现了Lock接口的锁, 
    它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力


    ReenreantLock类的常用方法有:

        ReentrantLock() : 创建一个ReentrantLock实例 
        lock() : 获得锁 
        unlock() : 释放锁 

5.5 使用局部变量实现线程同步 

    如果使用ThreadLocal管理变量,则每一个使用该变量的线程都获得该变量的副本, 
    副本之间相互独立,这样每一个线程都可以随意修改自己的变量副本,而不会对其他线程产生影响。

    常用方法:

    ThreadLocal() : 创建一个线程本地变量 
    get() : 返回此线程局部变量的当前线程副本中的值 
    initialValue() : 返回此线程局部变量的当前线程的"初始值" 
    set(T value) : 将此线程局部变量的当前线程副本中的值设置为value

 5.6 使用阻塞队列实现线程同步

LinkedBlockingQueue 类常用方法 

方式        抛出异常有返回值,不抛出异常阻塞 等待超时等待
添加addofferput        offer(等待时间,等待单位)
移除removepolltakepoll(等待时间,等待单位)

检测队首元素

elementpeekpeekpeek

5.7 使用原子变量实现线程同步

需要使用线程同步的根本原因在于对普通变量的操作不是原子的。
那么什么是原子操作呢?
原子操作就是指将读取变量值、修改变量值、保存变量值看成一个整体来操作
即-这几种行为要么同时完成,要么都不完成。

在java的util.concurrent.atomic包中提供了创建了原子类型变量的工具类,
使用该类可以简化线程同步。

其中AtomicInteger 表可以用原子方式更新int的值,可用在应用程序中(如以原子方式增加的计数器),
但不能用于替换Integer;可扩展Number,允许那些处理机遇数字类的工具和实用工具进行统一访问。

AtomicInteger类常用方法:
AtomicInteger(int initialValue) : 创建具有给定初始值的新的AtomicInteger
addAddGet(int dalta) : 以原子方式将给定值与当前值相加
get() : 获取当前值

 1 class Bank {
 2         private AtomicInteger account = new AtomicInteger(100);
 3 
 4         public AtomicInteger getAccount() {
 5             return account;
 6         }
 7 
 8         public void save(int money) {
 9             account.addAndGet(money);
10         }
11     } 

 6. JUC

什么是JUC:

Java真的可以开启线程吗? 开不了的,底层是用native关键词修饰.调用本地操作系统

6.1 lock锁

买票案例:

public class SyncSaleTicket {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        // 多个线程同时访问同一个资源,把资源放入线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 40; i++) {
                    ticket.sale();
                }
            }
        }, "a").start();
 
        new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 40; i++) {
                    ticket.sale();
                }
            }
        }, "b").start();
 
        new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 40; i++) {
                    ticket.sale();
                }
            }
        }, "c").start();
    }
}

// 资源类
class Ticket {
    private int ticketNum = 30;
 
    // 卖票
    public void sale() {
        if (this.ticketNum > 0) {
            System.out.println(Thread.currentThread().getName() + "购得第" + ticketNum-- + "张票, 剩余" + ticketNum + "张票");
            
            //增加错误的发生几率
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

顺序错误,上面问题就是多个线程同时访问资源引起的。

Lock使用:

// 1、创建锁
private Lock lock = new ReentrantLock();
// 2、加锁
lock.lock();
try {
     // 3、access the resource protected by the lock
    // 业务代码
} finally {
    // 4、解锁
    lock.unlock();
}

ReentrantLock构造器:默认是非公平的

6.2 synchronized和lock(ReentrantLock)锁的区别

1、synchronized是java关键字,他是jvm层面; lock是接口(相应的实现比如ReentrantLock);

2、synchronized无法获取到锁的状态, lock可以获取到锁的状态;

3、synchronized是自动锁,如果代码执行完毕或发生异常, 他会自动释放锁的资源, lock手动锁,需要人工进行调用unlock方法进行释放锁的资源, 一般都是放在finally; 如果不释放会发生死锁;

4、synchronized如果线程a获取到锁的资源,发生阻塞,线程b会一直等待, 而lock如果获取不到锁的资源,线程不用等待就结束了;

5、synchronized是可重入、不可中断, 非公平;  Lock 可重入、 可判断、 可公平(两则皆可);

6、Synchronized是基于内存屏障实现锁; ReentrantLock是基于AQS实现锁;

7、synchronized唤醒线程和等待线程则使用的是Object提供的notify()、wait();方法, 而lock是condition里面提供的signal()、await()方法、 而lock可以获取多个condition可以实现线程循序执行

 6.3 多线程的线程间通信

6.3.1 synchronized实现方式

这个案例主要实现让多个线程交替打印 0、1 这两个数字。incr 方法实现的增加1,当num不为0(此时为1)则进行wait等待,其余情况增加1,之后notifyAll唤醒其他等待的线程,这里使用while则是为了避免虚假唤醒的情况

class Share {
    private int num = 0;

    public synchronized void incr() throws InterruptedException {
        while (num != 0) {
            this.wait();
        }
        num++;
        System.out.println(Thread.currentThread().getName() + " :: " + num);
        this.notifyAll();
    }

    public synchronized void decr() throws InterruptedException {
        while (num != 1) {
            this.wait();
        }
        num--;
        System.out.println(Thread.currentThread().getName() + " :: " + num);

        this.notifyAll();
    }

}

public class test02 {
    public static void main(String[] args) {
        Share share = new Share();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        share.incr();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        },"线程A").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        share.decr();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        },"线程B").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        share.incr();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        },"线程C").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    try {
                        share.decr();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        },"线程D").start();
    }
}

 6.3.2 Lock + Condition实现方式

与上面的例子是一样的理解,只是这里实现方式变了。这里也是首先 lock.lock() 获取锁,incr 方法实现的增加1,当num不为0(此时为1)则进行await等待,其余情况增加1,之后signalAll唤醒其他等待的线程,这里使用while则是为了避免虚假唤醒的情况。最后为了确保锁一定能释放,将 lock.unlock() 写在finally子句中。

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

class Share2 {
    private int num = 0;

    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();

    public void incr() {
        lock.lock();
        try {
            while (num != 0) {
                condition.await();
            }
            num++;
            System.out.println(Thread.currentThread().getName() + " :: " + num);
            condition.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void decr() {
        lock.lock();
        try {
            while (num != 1) {
                condition.await();
            }
            num--;
            System.out.println(Thread.currentThread().getName() + " :: " + num);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

public class test03 {
    public static void main(String[] args) {
        Share2 share2 = new Share2();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    share2.incr();
                }
            }
        },"线程A").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    share2.decr();
                }
            }
        },"线程B").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    share2.incr();
                }
            }
        },"线程C").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    share2.decr();
                }
            }
        },"线程D").start();
    }
}

 6.3.3 Condition指定线程间的通信

这个案例实现的是:三个线程(a、b、c)循环交替打印,也就是线程a打印5行、线程b接着打印10行、线程c继续打印15行、线程a再打印5行......,这样一直循环下去。

首先是定义了一个标志位 flag(线程a对应1、线程b对应2、线程c对应3),三个方法中依次获取锁,然后满足条件就打印、不满足就等待,之后每打印完一次就修改下标志位为下一个打印的线程标志位,之后将要打印的线程唤醒即可。

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

public class test01 {
    public static void main(String[] args) {
        ShareResource shareResource = new ShareResource();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 3; i++) {
                    try {
                        shareResource.print5(i);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        },"线程a").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 3; i++) {
                    try {
                        shareResource.print10(i);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        },"线程b").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i <= 3; i++) {
                    try {
                        shareResource.print15(i);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        },"线程c").start();
    }
}
class ShareResource {
    //定义标志位
    //1 ---> a , 2 ---> b , 3 ---> c
    private int flag = 1;

    private Lock lock = new ReentrantLock();
    private Condition a = lock.newCondition(); // a
    private Condition b = lock.newCondition(); // b
    private Condition c = lock.newCondition(); // c

    //打印5次,参数为打印的轮数
    public void print5(int loop) throws InterruptedException {
        lock.lock();
        try {
            while (flag != 1) {
                a.await();
            }
            for (int i = 1; i <= 5; i++) {
                System.out.println(Thread.currentThread().getName() + " :: " + i + " , 轮数: " + loop);
            }
            flag = 2;
            b.signal();
        } finally {
            lock.unlock();
        }
    }

    //打印10次,参数为打印的轮数
    public void print10(int loop) throws InterruptedException {
        lock.lock();
        try {
            while (flag != 2) {
                b.await();
            }
            for (int i = 1; i <= 10; i++) {
                System.out.println(Thread.currentThread().getName() + " :: " + i + " , 轮数: " + loop);
            }
            flag = 3;
            c.signal();
        } finally {
            lock.unlock();
        }
    }

    //打印15次,参数为打印的轮数
    public void print15(int loop) throws InterruptedException {
        lock.lock();
        try {
            while (flag != 3) {
                c.await();
            }
            for (int i = 1; i <= 15; i++) {
                System.out.println(Thread.currentThread().getName() + " :: " + i + " , 轮数: " + loop);
            }
            flag = 1;
            a.signal();
        } finally {
            lock.unlock();
        }
    }
}

6.3.4 Volatile关键字

Volatile是Java虚拟机提供的轻量级同步机制(在单线程是用不到的)

1 保证可见性

2 不保证原子性

3 禁止指令重排

保证可见性:

可见性就是当一个线程 修改一个共享变量时,另外一个线程能读到这个修改的值。

不保证原子性:

什么叫原子性: 不可分割

线程A在执行任务的时候,是不能被打扰的,要么同时成功,要么同时失败

不保证原子性是指:

private volatile static int num=0;
public static void add() {
     num++;
}

num++操作不是原子性(++不是原子操作),可能多个线程同时操作

在Java中自增i++操作不是原子操作,它实际上包含三个独立的操作:

(1)读取i值;(2)加1;(3)将新值写回i

因此,如果并发执行自增操作,可能导致计算结果的不准确

解决方法:我们可以加synchronized

public synchronized static void add() {
     num++;
}

 禁止指令重排序

指令重排是指在程序执行过程中,为了提高性能, 编译器和CPU可能会对指令进行重新排序。

单线程环境里面确保程序最终执行结果和代码顺序执行结果一致。

处理器在进行指令重排序时必须考虑指令之间的数据依赖性

多线程环境中线程交替执行,由于编译器指令重排的存在,两个线程使用的变量能否保证一致性是无法确认的,结果无法预测。

Volatile则可以避免指令重排

使其加入了内存屏障,作用:

 1.保证特定操作的执行顺序

 2.可以保证某些内存表变量的可见性(利用这些特性Volatile实现了可见性)

 6.3.5 理解CAS

CAS: 全称Compare and swap,字面意思:”比较并交换“

CAS 是现代操作系统,解决并发问题的一个重要手段

CAS操作包括了3个操作数:

1、需要读写的内存位置(V)

2、进行比较的预期值(A)

3、拟写入的新值(B)

CAS操作逻辑如下:如果内存位置V的值等于预期的A值,则将该位置更新为新值B,否则不进行任何操作。

许多CAS的操作是自旋的:如果操作不成功,会一直重试,直到操作成功为止。

这里引出一个新的问题,既然CAS包含了Compare和Swap两个操作,它又如何保证原子性呢?  

答案是:CAS是由CPU支持的原子操作,其原子性是在硬件层面进行保证的。

6.4 死锁的发生原因和怎么避免

(如图) ,死锁 ,简单来说就是两个或者两个以上的线程在执行的过程中 ,争夺同一个共享资源造成的相互等待的现象。

如果没有外部干预,线程会一直阻塞无法往下执行,这些一直处于相互等待资源的线程就称为死锁线程。

导致死锁的条件有四个 ,也就是这四个条件同时满足就会产生死锁。

1. 互斥条件 ,共享资源 X 和 Y 只能被一个线程占用;

2. 请求和保持条件,线程 T1 已经取得共享资源 X在等待共享资源 Y 的时候,不 释放共享资源 X;

3. 不可抢占条件 ,其他线程不能强行抢占线程 T1 占有的资源;

4. 循环等待条件 ,线程 T1 等待线程 T2占有的资源 ,线程 T2 等待线程 T1 占有 的资源 ,就是循环等待。

导致死锁之后 ,只能通过人工干预来解决 ,比如重启服务 ,或者杀掉某个线程。

所以 ,只能在写代码的时候 ,去规避可能出现的死锁问题。

按照死锁发生的四个条件,只需要破坏其中的任何一个,就可以解决,但是,互斥条件是没办法破坏的 ,因为这是互斥锁的基本约束 ,其他三方条件都有办法来破坏:

1. 对于“请求和保持”这个条件,我们可以一次性申请所有的资源,这样就不存在等待了。

2. 对于“不可抢占”这个条件 ,占用部分资源的线程进一步申请其他资源时,如果申 请不到 ,可以主动释放它占有的资源 ,这样不可抢占这个条件就破坏掉了。

3. 对于“循环等待”这个条件,可以靠按序申请资源来预防。所谓按序申请,是指资 源是有线性顺序的, 申请的时候可以先申请资源序号小的 ,再申请资源序号大的, 这样线性化后自然就不存在循环了。

7. 线程池

7.1 什么是线程池

线程池其实就是一种多线程处理形式,处理过程中可以将任务添加到队列中,然后在创建线程后自动启动这些任务,这里的任务就是实现了Runnable或Callable接口的实例对象。

池化技术:

当程序允许时需要的链接如:JDBC链接,IO链接等等他们开启或关闭会极大的耗费资源,这时则引入了一个池化技术的概念。

池化技术本质:事先准备好一些资源,要用的时候来这里取,用完了在放回去,这样就不用一直开启关闭浪费资源了.

 使用线程池的好处:

  • 降低资源消耗
  • 效率大大提升
  • 方便我们管理

 7.2 三大方法

Executors.newSingleThreadExecutor(); //单线程

Executors.newFixedThreadPool(5);// 固定线程个数

Executors.newCachedThreadPool(); //缓存池,可扩展

/**
 * 三大方法
 */
public class TestExecutors {
    public static void main(String[] args) {
//        ExecutorService executorService = Executors.newSingleThreadExecutor();//单个线程
//        ExecutorService executorService = Executors.newFixedThreadPool(100);//固定线程池大写
        ExecutorService executorService = Executors.newCachedThreadPool();//可伸缩的线程池
        try {
            for (int i = 0; i < 100; i++) {
                executorService.execute(()->{
                    System.out.println(Thread.currentThread().getName());
                });
            }
        } finally {
            //线程池用完一定要关闭
            executorService.shutdown();
        }
    }

}

 7.3 七大参数

 public ThreadPoolExecutor(int corePoolSize, //常驻核心线程数
                           int maximumPoolSize, //最大线程数
                           long keepAliveTime, //空闲线程存活时间(数字)
                           TimeUnit unit, //空闲线程存活时间(单位)
                           BlockingQueue<Runnable> workQueue, //任务队列
                           ThreadFactory threadFactory, //线程工厂,一般不动
                           RejectedExecutionHandler handler); //拒绝策略

核心线程

线程池一创建号就会创建并开启这些线程

最大线程数的确定的方法 

最大线程数为这个线程池能创建的最大线程数量

1)CPU密集型:看本机的逻辑CPU为几核最大线程数就为几

原因:因为这样可以达到最大并行的线程数,效率最高

附:Runtime.getRuntime().availableProcessors();//获取cpu核数

2)IO密集型:先判断程序中十分消耗IO的线程数量,最大线程数再乘以2

原因:IO操作耗时量大,并且占用资源也大,所以先分配同等数量的线程去执行这些IO线程,再分配大于该数量的线程,一般是2倍

空闲线程存活时间

除开核心线程数,剩余的线程过了设置的空闲线程存活时间后,会自杀,但核心线程你主动关闭是不会自杀的

任务队列

存放等待执行线程的队列,一般用BlockingQueue阻塞队列,除此之外还有BlockingDueue双端队列、SynchronousQueue同步队列等等

线程工厂

线程池的线程由线程工厂创建,一般默认Executors.defaultThreadFactory(),

拒绝策略

当线程数量大于最大线程数+任务队列时,所采取的策略
 

四种拒绝策略:

AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行。

队列满了(大于最大线程数+阻塞队列),还有任务进来,不处理这个任务,抛出异常!

DiscardPolicy:丢弃无法处理的任务,不会抛出异常。(如果允许任务丢失,这是最好的一种策略)

队列满了(大于最大线程数+阻塞队列),还有人进来,不处理这个人的,不会抛出异常!

DiscardOldestPolicy:抛弃队列中等待最久的任务,重新提交当前任务。

队列满了(大于最大线程数+阻塞队列),尝试去和等待最久的任务竞争,也不会抛出异常!

CallerRunsPolicy(调用者运行):由调用线程(提交任务的线程)处理该任务。

队列满了(大于最大线程数+阻塞队列),哪来的去哪里(从main线程来的就该给main线程自己去处理)!也不会抛出异常!

7.4 创建线程池实例

创建线程池的代码:

package threadpool;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * wait:进入等待池并释放锁
 * notify:让某个线程从等待池进入锁池
 */

public class FixedThreadPool {
    //任务列表
    private BlockingQueue<Runnable> tasks;
    //线程集合
    private List<Thread> threads;

    public FixedThreadPool(int taskSize,int pollSize){
        //初始化任务列表
        //LinkedBlockingQueue:阻塞队列,内部有两个ReentrantLock用于put方法和take方法
        this.tasks=new LinkedBlockingQueue<>(taskSize);
        //初始化线程集合
        this.threads= Collections.synchronizedList(new ArrayList<>());

        //初始化线程
        for (int i = 0; i < pollSize; i++) {
            //创建线程(pool中包含了tasks)
            Thread worker=new WorkerThread(this);
            //启动线程
            worker.start();
            //将线程添加到集合
            this.threads.add(worker);
        }
    }

    //工作线程:从任务里列表中不断取任务以执行
    public class WorkerThread extends Thread{
        private FixedThreadPool pool;

        public WorkerThread(FixedThreadPool pool) {
            this.pool = pool;
        }

        @Override
        public void run(){
            Runnable task=null;
            //取任务
            while (this.pool.isWorking||this.pool.tasks.size()>0){
                try {
                    //阻塞方法
                    if (this.pool.isWorking){
                        task=this.pool.tasks.take();
                    }
                    //非阻塞方法
                    else{
                        task=this.pool.tasks.poll();
                    }

                    System.out.println("线程"+getName()+"已取任务");
                    System.out.println("任务列表任务数"+this.pool.tasks.size());
                } catch (Exception e) {
                    e.printStackTrace();
                }

                //执行
                if (null != task){
                    task.run();
                    System.out.println("线程"+getName()+"已执行完毕");
                }
            }
        }
    }

    //添加任务的方法
    //阻塞
    public void usePut(Runnable task){
        if (isWorking){
            try {
                this.tasks.put(task);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    //非阻塞
    public Boolean useOffer(Runnable task){
        if (isWorking){
            return this.tasks.offer(task);
        }

        else{
            return false;
        }
    }



    private volatile Boolean isWorking=true;
    //关闭线程池的方法
    public void close(){
        isWorking=false;
        for(Thread worker: this.threads){
            if (worker.getState().equals(Thread.State.BLOCKED)||worker.getState().equals(Thread.State.WAITING)){
                worker.interrupt();
            }
        }
    }
}

测试:

package threadpool;

public class FixedThreadPoolTest {
    //不能用单元测试(单元测试方法结束后不再执行子线程)
    public static void main(String[] args) {
        //创建一个线程池
        FixedThreadPool fixedThreadPool = new FixedThreadPool(10, 3);

        //添加任务
        for (int i = 0; i < 30; i++) {
            System.out.println("尝试添加任务");
            fixedThreadPool.usePut(()->{
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });

            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        //关闭线程池
        fixedThreadPool.close();
    }
}



 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值