您的位置 首页 java

Java 核心基础知识总结(下)

并发历史的演进及深入理解

在计算机早期,程序运行只能从上到下去执行,计算机的所有资源都为这个程序服务,不管你需不需要都得给我候着,这个时候就会存在一个系统资源利用不充分的情况,在这个背景下操作系统出现了,它给我们的程序带来的并发性,使得我们可以在一边听歌的情况下一边写文章,还能一边聊微信、QQ。那么操作系统解决了程序运行的那些问题呢?主要有以下三点:

  • 提升资源的利用率 :由于单个程序存在资源浪费的情况,如果我们在等待程序的时候可以运行另外一个程序,这样将会大大提高系统资源的利用率。
  • 公平性 :操作系统会为不同的程序划分时间片来使用资源,使得所有的程序都能够使用相对于的资源,但是有点要注意的是,在操作系统中我们可以对进程的优先级进行调整,使得当某个进程释放后同时会有更高优先级的进程去抢夺资源。在资源足够的情况下还无所谓,但是在资源不够的情况下就会产生一个问题,那就是优先级低的进程无法获得资源。
  • 便利性 :多进程的能够及时进行信息相互通讯可以避免信息孤岛,避免的一些重复性的工作。

在实际的开发过程中,在一定程度上来说,串行编程具有直观性和简单性,但是随着计算机的发展,传统的串行编程已经不适合时代的发展了,这个时候多进程出现了,同时也促使着线程的出现。在 Java 中线程是一种轻量级的进程,它的创建和销毁比进程的开销都要小很多。因此在本文中我们主要将精力放在对线程的研究。

线程的理解及在 Java 中如何去应用

多线程

多线程应用程序就像是具有多个 CPU 同时执行应用程序代码。 由于 CPU 的执行速度非常的快,单个 CPU 在多个线程间共享 CPU 时间片,在给定的时间段中线程会进行切换,所以我们常会认为是多个 CPU 同时在进行代码执行,但是多个线程可以分布在多个不同的 CPU 上执行。我们来画个图表示下:

并发和并行的关系

并发意味着应用程序会执行多个任务,但是如果计算机只有一个 CPU 的话,那么应用程序无法同时执行多个任务,但是应用程序又需要执行多个任务,所以计算机在执行下一个任务前,它会将当前的执行状态暂存,进行业务切换,直到业务完成。 并行是指应用程序将其任务分解成较小的子任务,这些子任务可以并行同时处理,例如在多个 CPU 上同时执行。

线程安全性问题

线程安全性是非常复杂的,当没有采用同步机制的情况下,多线程的执行操作结果往往是不可预测的。当多个线程访问某个类时,如果这个类始终都表现出正确的行为,那么这个类时线程安全的,反之则是不安全的。下面给出一段代码,我们看看线程安全性的问题体现在哪里。

 public class Tsyn implements Runnable {
    public static int count = 0;

    public void increase(){
        count++;
    }

    @Override
    public void run() {
        for(int x=0;x<1000;x++){
            increase();

        }

    }


    public static void main(String[] args) {

        Tsyn tsyn = new Tsyn();

        Thread t1 = new Thread(tsyn);
        Thread t2 = new Thread(tsyn);

        t1.start();
        t2.start();
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("count===>"+count);

    }
}
  

通过执行这段程序我们会发现每次 i 的值都不一样,这个并不符合我们的预测。这里主要是 i 是静态变量,在没有经过任何线程安全的措施保护,多个线程会并发的去修改 i 的值,所以这里 i 是不安全的,而导致种结果的原因是由于 t1 和 t2 读取的 i 值彼此不可见,所以这是由于可见性导致线程的安全问题。另外多线程还会带来原子性、有序性、活跃性、性能等问题。

那么我们如何去解决线程安全问题呢?

其实主要就是要对状态访问操作进行管理,一般情况下,主要就是共享和可变的的变量才会出现问题,共享意味着某个变量可以被多线程同时访问,可变意味着在生命周期内会发生变化。因此一个变量是否安全,主要取决于他是否被多个线程访问,如果要使变量能够被安全的访问,那必须通过同步机制来对变量进行修饰。而如果不使用同步机制使线程安全,那就要避免多线程对共享变量的访问,如不要再多线程之间共享变量,将变量置为不可变。

线程安全问题的解决:

  • 原子性:不可分割 、要么全部执行成功要么全部执行失败。
  • 静态条件:当多个线程同时对一共享数据进行修改,从而影响程序运行的正确性,线程切换时导致竟态条件出现的诱导因素。
  • 加锁机制:对方法进行加锁、对某个对象进行加锁、对类对象进行加锁。
  • 使用线程安全的集合工具。

关于如何解决线程安全的问题我们下面会讲到。

线程的创建

继承 Thread 类来创建线程

 //定义一个线程类使其继承 Thread 类,并重写 run() 方法
public class Test1Thread extends Thread {

    static int count1;

    /**
     * run 方法内部就是线程要完成的任务,因此 run 方法也被称为执行体
     */    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {

            count1++;
        }
    }


    public static void main(String[] args) throws InterruptedException {
        Test1Thread test1Thread = new Test1Thread();

        //启动方法要注意并不是调用 run 方法来调用线程,而是采用 start 方法来调用线程
        test1Thread.start();
        //调用 join 方法使 main 线程等待 test1Thread 执行完毕再继续执行
        test1Thread.join();
        System.out.println("count1=>>"+count1);
    }
  

使用 Runnable 接口来创建线程

线程在使用实现 Runnable 的同时也能实现其他接口,非常适合多个相同线程来处理同一份资源的情景,体现了面向对象的思想。

 //重写 Runnable 接口的 run 方法,也是该线程的执行体
public class TThreadUseRunnable  implements Runnable {

    static int count2;

    @Override
    public void run() {

        for (int i = 0; i < 1000; i++) {

            count2++;
        }

    }

    public static void main(String[] args) throws InterruptedException {

        //创建线程实例
        Thread t1 =  new Thread(new TThreadUseRunnable());
        //调用 start 方法来启动该线程
        t1.start();
        t1.join();
        System.out.println("count2----->"+count2);
    }
}
  

使用 Callable 接口来创建线程

如果你希望线程任务完成后能够返回一个值的话,那你可以实现 Callable 接口。

 //继承 Callable 接口并重写 call 方法
public class TestUseCallable implements Callable {

    static int count3;

    public CallableTask(int count3) {


        this.count3 = count3;
    }

    /**
     * 重写 Call 方法且可以抛出异常
     * @return
     * @throws Exception
     */    @Override
    public Object call() throws Exception {
        return count3;
    }


    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<Integer> task = new FutureTask((Callable<Integer>)()->{
            for (int i = 0; i < 100; i++) {

                count3++;

            }

            return count3;
        });

        Thread thread = new Thread(task);
        thread.start();

        Integer total = task.get();

        System.out.println("total====>"+total);
    }
}
  

使用 Callable 接口既能够实现多个接口,也能够得到执行结果的返回值。

使用线程池来创建线程

使用线程池的优势:

  • 使用线程池可以复用线程,控制最大并发数
  • 可以实现线程队列的缓存策略和拒绝机制
  • 实现某些与时间相关的功能,如定时任务
  • 隔离线程环境,避免线程相互影响

CacheThreadPool

 public static void main(String[] args) throws InterruptedException {

        //创建线程实例
        Thread t1 =  new Thread(new TThreadUseImplement());

        //CachedThreadPool 会为每个任务创建线程
        ExecutorService service = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {

            service.execute(t1);

        }

        service.shutdown();
    }
  

FixedThreadPool 可以使用有限的线程集来启动多线程,可以用来限制线程数量,可以节省时间,而且不必为每个任务都福鼎付出创建线程的开销。

  public static void main(String[] args) throws InterruptedException {

        //创建线程实例
        Thread t1 =  new Thread(new TThreadUseImplement());

        ExecutorService service = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 5; i++) {

            service.execute(t1);

        }

        service.shutdown();
    }
  

SingleThreadExecutor 就是线程数量为 1 的 FixedThreadPool,如果向 SingleThreadExecutor 提交多个任务,这些任务将会排队,每个任务都会在下一个任务开始前结束且所有的任务都使用相同的线程。

     public static void main(String[] args) throws InterruptedException {

        //创建线程实例
        Thread t1 =  new Thread(new TThreadUseImplement());

        ExecutorService service = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 5; i++) {

            service.execute(t1);

        }

        service.shutdown();
    }
  

从执行结果可以看到每个任务都是挨着执行的,没有之前那种换进换出的效果。

线程休眠

在多线程中让线程休眠的简单方式就是 sleep(),一般有两种一种是 TimeUnit 还有一种就是 Thread.sleep(),在这里我个人建议使用 TimeUnit,废话不多说,我们直接上代码:

 package com.zhangpan;

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

public class TestThread1 implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread()+"start....");

        for (int i = 0; i < 5; i++) {
            if (i==3){
                System.out.println(Thread.currentThread()+"sleep...");
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        System.out.println(Thread.currentThread()+"workeup and end....");
    }

    public static void main(String[] args) {

        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {

            executorService.execute(new TestThread1());
        }
        executorService.shutdown();
    }
}
  

线程优先级

由于线程调度器对每个线程的执行都是不可预知的,那么我们如何告诉线程调度器那个任务要被优先执行呢?

在这里我们可以通过设置线程的优先级状态,告诉线程调度器那个线程执行的优先级比较高,让线程调度器倾向于优先级比较高的线程优先执行。

 package com.zhangpan;

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

public class simpleThread implements Runnable {

    private int priority;

    public simpleThread(int priority) {
        this.priority = priority;
    }

    @Override
    public void run() {

        Thread.currentThread().setPriority(priority);

        for (int i = 0; i < 200; i++) {
            System.out.println(this);
            if (i % 10 ==0){

                //线程让步
                Thread.yield();
            }
        }
    }

    @Override
    public String toString() {
        return Thread.currentThread()+"simpleThread{" +
                "priority=" + priority +
                '}';
    }

    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {

            //默认最大优先级为 10
            service.execute(new simpleThread(Thread.MAX_PRIORITY));

        }
        service.execute(new simpleThread(Thread.MIN_PRIORITY));
    }
}
  

通过输出可以看到最后一个优先级最低,其余的线程优先级最高,这里注意的是优先级是在 run 开头设置的,不要再构造器中设置,这个是很多初学者容易犯的错误。

一般情况下,我们只使用ํMAX_PRIORITY、NORM_PRIORITY、MIN_PRIORITY 这三种级别。

在上述代码中,我们看到了有一个 yield 方法,yield 方法是建议执行切换 CPU,而不是强行执行 CPUi 切换,这一点大家要注意。

守护线程

守护线程是指程序运行在后台的一种服务线程,这种线程不是必须的,当其他前台程序停止的时候,同时会终止后台程序,反而言之,只要有其他任何非后台线程还在执行,守护线程就不会停止,我们看一段代码:

 package com.zhangpan;

import java.util.Arrays;
import java.util.concurrent.TimeUnit;

public class DaemoThread implements Runnable {
    @Override
    public void run() {
        while (true){

            try {
                TimeUnit.MILLISECONDS.sleep(200);
                System.out.println(Thread.currentThread()+" "+this);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i < 10; i++) {

            Thread daemon = new Thread(new DaemoThread());
            daemon.setDaemon(true);
            daemon.start();

        }

        System.out.println("守护线程启动");
        TimeUnit.MILLISECONDS.sleep(300);
    }
}
  

在上述代码中,线程每次循环会创建 10 个线程,每个线程都会设置为后台线程,for 循环会进行十次然后输出信息,随后主线程睡眠一段世家后停止运行,每次 run 循环中,都会打印当前线程,当主线程运行完毕后程序就执行完毕了,因为 daemon 时后台线程,无法影响主线程执行。

Join() 线程加入

如果某个线程在另一个线程上调用 join() 方法,那么此线程将被挂起,知道目标结束才会回复,这里我们可以用 isAlive() 返回真假判断。

 import java.util.concurrent.TimeUnit;

public class TestJoinThread extends Thread {

    @Override
    public void run() {

        for (int i = 0; i < 5; i++) {
            try {
                TimeUnit.MICROSECONDS.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread()+" "+i);


        }
    }

    public static void main(String[] args) throws InterruptedException {

        TestJoinThread tjoin1 = new TestJoinThread();
        TestJoinThread tjoin2 = new TestJoinThread();
        TestJoinThread tjoin3 = new TestJoinThread();

        tjoin1.start();

        tjoin1.join();
        tjoin2.start();
        tjoin3.start();

    }
}
  

join() 方法会导致当前运行的线程停止执行,直到它加入的线程完成其任务。

同步容器类

同步容器类主要有两种,一种本来就是线程安全的容器,如 Vector、HashTable、Stack,这类容器都加了 synchronized 锁,但是由于其效率不高,所以这些容器我们现在几乎不再使用 还有一种就是使用线程安全的集合来实现,这里使用 Collections.synchronized 来将那些非线程安全的容器封装起来使其线程安全。从下列源码可以看出这些线程安全。

系统并发工具

在说 JUC 之前我们先回顾一下系统级别的并发工具:

  • 信号量 :信号量是 Dijkstra 在 1965 年提出的一种方法,它主要是使用一个整形变量来累计唤醒次数,以供之后使用。他认为有一个新的变量类型称之为信号量。一个信号量可以是 0 或者是任意正数。0 表示不需要唤醒,任意正数表示的是唤醒次数。
  • 互斥量 :互斥量是信号量的一个简单版本,它的优势在于在一些共享资源何一段代码中保持互斥。
  • 管程 :管程是一种高级同步原语,有一个很重要的特性就是在任何时候它只有一个进程,正是因为这个原因,使管程可以很方便的实现互斥操作。
  • 屏障 :屏障这种同步机制是准备用于进程组而不是进程间的生产者—消费者的。一般情况下我们会在某些应用中划分了若干阶段,规定除非所有的进程都准备着手下一阶段,否则任何进程都不能进入下一个阶段,我们可以通过每个阶段的结尾安装一个屏障来实现这种行为。

Java 并发工具包

JDK 1.8 提供了许多并发容器来改进同步同期的性能,同步容器将所有对容器的操作访问都串行化从而实现线程安全性。下面我们来看看 Java 中都用了那些并发的小工具。

ConcurrentHashMap

 public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>implements ConcurrentMap<K,V>, Serializable
  

我们在代码中可以看到,ConcurrentHashMap 继承了 AbstractMap 并实现了 ConcurrentMap 和 Serializable 接口 ConcurrentHashMap 采用的是多把锁,每把锁只用来锁一段数据,当多个线程访问不同数据段时就不会存在竞争关系。这样的好处激素 hi 在并发环境下可以实现更高的吞吐量,单线程环境下性能损失比较小。

ConcurrentMap

ConcurrentMap 是一个接口,它继承了 Map 接口并提供了 Map 接口中的四个新方法,而且这四个方法都是原子性方法。

 public interface ConcurrentMap<K, V> extends Map<K, V> {

        V putIfAbsent(K key, V value);

        boolean remove(Object key, Object value);

        boolean replace(K key, V oldValue, V newValue);

        V replace(K key, V value);
}
  

ConcurrentSkipListMap

ConcurrentSkipListMap 是线程安全的有序哈希表,适用于高并发环境,其底层数据结构是基于跳表实现的,ConcurrentSkipListMap 可以提供 Comparable 内部排序或者 Comparator 外部排序。

ConcurrentSkipListSet

ConcurrentSkipListSet 是线程安全的有序集合,适用于高并发环境,底层是通过 ConcurrentNavigableMap 来实现的,是一个有序的线程安全集合。

CopyOnWriteArrayList

CopyOnWriteArrayList 是 ArrayList 的变体,CopyOnWriteArrayList 中所有的可变操作,如 add,set 其实都是重新创建了一个副本,通过对数组的复制来实现线程安全。当一般高并发环境,读操作远多于写操作的时候,为了保证线程安全一般建议使用 CopyOnWriteArrayList。

BlockingQueue

BlockingQueue 在检索元素的时候会等待队列变成非空,并在存储元素时等待队列变为可用。值得注意的时,BlockingQueue 不允许添加 null 元素,当使用 add、put、offer 方法添加 null 会抛出空指针异常,且有容量限制。BlockingQueue 有多种实现分别如下:

  • LinkedBlockingQueue:基于链表构造,先入先出的阻塞队列。链表队列一般比基于数组的队列吞吐量较高,但是在并发应用中,可预测的性能一般。
  • ArrayBlockingQueue:基于数组实现,按照先入先出的原则进行排序。默认不保证线程公平访问队列。
  • PriorityBlockingQueue:支持优先级的阻塞队列,默认情况下采用自然顺序升序或者降序。
  • DelayQueue:支持延时获取元素的无阻塞队列,元素只能在延迟到期后才能使用。
  • TransferQueue:他是一个接口,生产者会一直阻塞直到所添加的到队列的元素被某一个消费者所消费。
  • LinkedTransferQueue:继承 TransferQueue,基于链表的无界的 TransferQueue。head 是队列中存在时间最长的元素,tail 是队列中存在时间最短的元素。

BlockingDeque

BlockingDeque 是一个双端队列,分别实现了在队列头和队列尾的插入,其实现有 LinkedBlockingDeque。

  • LinkedBlockingDeque:由链表结构组成的双向阻塞队列,可以从队列的双端插入和移除元素。双端队列因为多了一个队列操作入口,因此在多线程同时入队时也减少了一半的竞争。

同步工具类

Semaphore 信号量

在 Java 中,Semaphore 是用来控制同时访问特定的资源的线程数量,通过协调各个线程以保证合理的使用公共资源,废话不多说。上代码。

 package com.zhangpan;

import java.util.Arrays;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class SemaphoreTest {

    public static void main(String[] args) {
        //银行有两个窗口办业务,有人走其他人才能办
        final Semaphore semaphore = new Semaphore(2);

        for (int i = 0; i < 8; i++) {
            final int t = i;

            new Thread(()->{
                try {
                    semaphore.acquire();
                    System.out.println(Thread.currentThread().getName()+"到窗口办业务");
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println(Thread.currentThread().getName()+"业务办完离开");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {

                    //释放
                    semaphore.release();
                }

            },String.valueOf(t)).start();
        }

    }
}
  

结果:

CountDownLatch 减法计数器

CountDownLatch 减法计数器是闭锁的一种实现,闭锁有一个计数器,且需要对其初始化,表示需要等待的次数,闭锁在调用 await 处进行等待,其他线程在调用 countDown 把闭锁 count 次数进行递减,直至递减为 0,从而唤醒 await。话不多说,上代码。

 package com.zhangpan;

import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.FutureTask;

public class countDownlatch {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch count = new CountDownLatch(2);
        for (int i = 0; i < 5; i++) {
            final int t = i;
            new Thread(new FutureTask(()->{
                System.out.println(Thread.currentThread().getName()+">>>出去");
                count.countDown();
                return null;
            }),String.valueOf(t)).start();
        }

        count.await();


        System.out.println("所有线程执行完毕,关闭主线程");
    }
}
  

我们将技术初始为 5 再看看:

 package com.zhangpan;

import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.FutureTask;

public class countDownlatch {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch count = new CountDownLatch(5);
        for (int i = 0; i < 5; i++) {
            final int t = i;
            new Thread(new FutureTask(()->{
                System.out.println(Thread.currentThread().getName()+">>>出去");
                count.countDown();
                return null;
            }),String.valueOf(t)).start();
        }

        count.await();


        System.out.println("所有线程执行完毕,关闭主线程");
    }
}
  

CyclicBarrier 累计计数器

CyclicBarrier 累计计数器和闭锁很类似,它也是阻塞一组线程直到某个事件的发生。

 package com.zhangpan;

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

public class cyclicBarrierTest {

    public static void main(String[] args) {
        CyclicBarrier cyclicBarrier = new CyclicBarrier(10,()->{

            System.out.println("存够 10 万,可以付买车首付了");
        });

        for (int i = 0; i < 10; i++) {
            final int t = i+1;

            new Thread(()->{
                System.out.println("存了"+t+"W");
                try {
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}
  

Java 锁分类及其理解

在 Java 中锁有很多,我们可以按照不同的功能、种类进行分类,我们来看一张图:

下面对每个锁分类进行详细讲解。

线程是否需要对资源进行加锁

在 java 中按照是否对资源进行加锁一般分为乐观锁和悲观锁,乐观锁和悲观锁不是那种实现的锁,而是一种设计的思想,我们在学习多线程和数据库的时候经常需要用到这种设计思想,下面我们分别来讲讲乐观锁和悲观锁的优缺点及区别。

悲观锁

悲观锁是一种悲观思想,它总认为数据可能会被其他人修改,所以悲观锁在持有数据的时候会将他锁住,不让其他人访问,其他线程想要访问就会阻塞,知道悲观锁将资源释放。其中关系型数据库中就用到了很多这种锁机制。比如行锁,表锁,读锁,写锁等。这种都是依赖数据库本身的功能进行实现。在 java 多线程中,Synchronized 和 ReentrantLock 等独占锁就是一种悲观锁的思想。悲观锁因为读写都加锁,所以其性能相对也比较低,在互联网高并发的环境下,悲观锁实现的是越来越少了。但是在一些多读的情况下还是需要悲观锁的。

乐观锁

乐观锁的思想和悲观锁的思想相反,它认为资源不会被别人所修改,在读取的时候不会对资源进行上锁,但是在写入的时候它会判断数据是否被修改过,那么他是如何判断呢?一般方案有两种,分别是版本号机制和 CAS。在数据库中乐观锁会使用版本号来检查数据是否被修改过,在 Java 中,java.util.concurrent.atomic 原子变量类就是使用乐观锁的一种实现方式。乐观锁一般适用与读多写少的情况,即很少发生冲突的场景,这样可以省去锁的开销,增加系统的吞吐量。

乐观锁的实现方式

版本号机制 :版本号机制是在数据表中加一个 version 字段来实现,表示数据被修改的次数,当执行写操作的时候且写入成功后 version = version + 1。我们来看个例子。

事务一开启,我们先在 ATM 机上进行操作存钱:

 begin
update money_table set money = 120,version = version + 1 where money = 100 and version = 0
  

此时金额改为 120,版本号为 1,事务未提交

事务二开启,我们通过网银取钱

 begin
update money_table set money = 50,version = version + 1 where money = 100 and version = 0
  

此时金额改为 50,版本号为 1 ,事务未提交

现在提交事务一,金额改为 120,版本变未 1,提交事务。理想情况下金额变为 50,版本变为 2。但是事务二的更新是建立在金额为 100 版本号为 0 的基础上的,所以事务二不会提交成功,需要重新提取金额和版本号,再次进行写操作。

CAS 算法

CAS 全称为 compare and swap,是一种有名的无锁算法。即在不适用锁的情况下实现多线程之间的变量同步,也就是没有线程在阻塞的情况下实现变量同步,也叫非阻塞同步。CAS 中涉及三个要素:

  • 需要写的内存值 V
  • 进行比较的值 A
  • 准备写入的新值 B

只有当预期值 A 和内存值 V 相同时,这个时候内存值 V 会修改为 B。

话不多说,我们来看个例子:

 package com.zhangpan;

import java.util.Arrays;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.atomic.AtomicInteger;

public class TestAtomic {

    public static volatile AtomicInteger a = new AtomicInteger(0);

    public static void main(String[] args) {

        CyclicBarrier cyclicBarrier = new CyclicBarrier(10,()->{

            System.out.println(a);//10000
        });

        for (int i = 0; i < 10; i++) {

            new Thread(()->{

                try {
                    for (int q = 0;q<1000;q++)a.getAndIncrement();
                    cyclicBarrier.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}
  

经测试可得,每次的值都是 10000,即在多线程并行的情况下,适用 AtomicInteger 可以保证线程的安全性。

乐观锁的缺点

  • ABA 问题:多个线程操作同一个值,1 号线程将设定值设定为 A,2 号线程将原值 A 修改为 B,最后修改为 A,这个时候 1 号线程就无法判断这个值是否被修改过。
  • 循环增加开销:因为乐观锁如果写入不成功将会触发等待,使其重试。这种情况是一个自旋锁,简单来说就是适用于短期内获取不到,进行等待重试的锁,它不适用于长期获取不到锁的情况且自旋循环对于性能开销比较大。

资源已被锁定,线程是否阻塞

在多线程环境下有时候需要互斥访问,这个时候就需要引入锁的概念,只有获取了锁的线程才能够对资源进行访问。由于同一个时刻只能有一个线程获取到锁,那么没有获取到锁的线程该怎么办呢?

一般有两种方式。一种是线程一直循环等待该资源是否释放锁,这种锁叫自旋锁,它不会将线程阻塞起来。还有一种方式是将自己阻塞起来,等待重新调度请求,这种叫互斥锁。

自旋锁

自旋锁的定义:当一个线程尝试去获取某一把锁的时候,这个锁已经被别人占用,那么此线程就无法获取到这把锁,那么该线程就会自旋,那么如何等待呢?执行一段无意义的循环即可(自旋),间隔一段时间后会再次尝试获取。这种循环加锁-等待的机制叫做自旋锁下面我们来看代码如何实现自旋锁

 package com.zhangpan;

import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

public class SpinLock {

    private AtomicReference<Thread> reference = new AtomicReference<>();

    public void lock(){

        while (!reference.compareAndSet(null,Thread.currentThread()));
    }

    public void unlock(){
        reference.compareAndSet(Thread.currentThread(),null);
    }
}

class Test{
    public static void main(String[] args) throws InterruptedException {

        SpinLock lock = new SpinLock();
        new Thread(()->{
            lock.lock();
            try {
                System.out.println("A 线程拿到锁");
                TimeUnit.SECONDS.sleep(5);
            }catch (InterruptedException e){

                e.printStackTrace();
            }finally {

                lock.unlock();
                System.out.println("A 线程锁释放");
            }
        }).start();

        //TimeUnit.SECONDS.sleep(2);

        new Thread(()->{

            lock.lock();

            try{

                System.out.println("B 线程拿到锁");
            }finally {

                lock.unlock();
                System.out.println("B 线程锁释放");
            }

        }).start();
    }
}
  

这种自旋锁有一个问题,它无法保证多线的竞争公平性,当出现很多线程的时候,有可能会导致某些线程一直都未获取到锁从而导致线程饥饿,通常为了解决这类问题,我们有了排队自旋锁。

TicketLock

TicketLock 是一种同步机制的的自旋锁,它采用 Ticket 来控制线程的执行顺序。TicketLock 基于 FIFO 的队列机制,增加了锁的公平性,TicketLock 有两个 int 类型的数值,开始都是 0,第一个是队列 ticket,第二个是出队票据。简单来说就是队列票据是你取票号的位置,出对票据是你距离叫号的位置,下面我们来代码实现一下。

 import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class TicketLock {

    //队列票据,当前排队的号码
    private AtomicInteger queueNum = new AtomicInteger();

    //出队票据,当前需要等待的号码
    private AtomicInteger deueNum = new AtomicInteger();

    public int lock() {
        int currentTicketNum = deueNum.incrementAndGet();
        while (currentTicketNum != queueNum.get()){
            System.out.println("开始办业务");
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        return currentTicketNum;
    }

    public void unlock(int ticketNum){

        //释放锁,传入当前排队的号码
        queueNum.compareAndSet(ticketNum,ticketNum+1);
    }

}
  

每次叫号机在叫号的时候,都会判断是不是被叫的号,并且每个人在办完业务的时候,叫号机会根据当前业务在前号码上的基础上 +1,让队列继续走。

TicketLock 缺点

虽然解决了公平性的问题,但是每次读写操作都必须在多个处理器之间进行缓存同步,这样会导致繁重的系统总线和内存流量,降低系统整体性能。

CLHLock

CLHLock 是基于链表设计,公平的,高性能的自旋锁,CLH 实际上是指三个人:Craig、Landin 和 Hagersten,大家不关心这三个人是谁,只需要关心怎么用即可。

 public class CLHLock {
     public static class CLHNode{
     private volatile boolean isLocked = true;
 }
 // 尾部节点
     private volatile CLHNode tail;
     private static final ThreadLocal<CLHNode> LOCAL = new ThreadLocal<>();
     private static final AtomicReferenceFieldUpdater<CLHLock,CLHNode> UPDATER =AtomicReferenceFieldUpdater.newUpdater(CLHLock.class,CLHNode.class,"tail");
 public void lock(){
 //新建节点并将节点与当前线程保存
 CLHNode node = new CLHNode();
 LOCAL.set(node);
 // 将 queue 设置为当前节点,且返回之前的节点
 CLHNode preNode = UPDATER.getAndSet(this,node);
 if(preNode != null){
 // 如果之前的节点不为 null,表示锁被其他的线程吃所有
 while (preNode.isLocked){
 }
 preNode = null;
 LOCAL.set(node);
 }
 // 循环判断,表示当前节点标志位为 false
 }
 public void unlock() {
        //获取当前线程对应的节点
         CLHNode node = LOCAL.get();
         // 如果 tail 节点等于 node,则将 tail 节点更新为 null,同时将 node 的 lok 状态置为 false
         if (!UPDATER.compareAndSet(this, node, null)) {
                 node.isLocked = false;
         }
         node = null;
 }
}
  

MCSlock

MCS Spinlock 使一种基于链表的搞扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,与 CLH 的区别使,CLH 是在前驱节点的 locked 域上自旋,MCS 是在自己节点上的 locked 域上自旋。

下面看看代码实现。

 public class MCSLock {
    volatile Node head, tail;//waitingList

    public MCSLock() {
        head = tail = null;
    }

    public Node lock() {
        //lock-free 的将 node 添加到 waitingList 的尾部
        Node node = new Node(true, null);
        Node oldTail = tail;
        while (!cas(tail, oldTail, node)) {
            oldTail = tail;
        }

        if (null == oldTail) {//如果等待列表为空,那么获取锁成功,直接返回
            return node;
        }

        oldTail.setNext(node);
        while (node.isLocked()) {//监听当前节点的 locked 变量
        }

        return node;
    }

    public void unlock(Node node) {
        if (node.getNext() == null) {
            if (cas(tail, node, null)) {//即使当前节点的后继为 null,也要用 cas 看一下队列是否真的为空
                return;
            }
            while (node.getNext() != null) {//cas 失败,说明有后继节点,只是还没更新前驱节点的 next 域,等前驱节点看到后继节点后,即可安全更新后继节点的 locked 域

            }
        }
        node.getNext().setLocked(false);
    }

    static class Node {
        public Node(boolean locked, Node next) {
            this.locked = locked;
            this.next = next;
        }

        volatile boolean locked;//true:当前线程正在试图占有锁或者已经占有锁,false:当前线程已经释放锁,下一个线程可以占有锁了
        Node next;//后继节点

        public boolean isLocked() {
            return locked;
        }

        public void setLocked(boolean locked) {
            this.locked = locked;
        }

        public Node getNext() {
            return next;
        }

        public void setNext(Node next) {
            this.next = next;
        }
    }
  

CLHlock 和 MCSLock 区别与共同点:

  • 基于链表,CLH 使基于隐式链表,没有真正的后续节点属性,MCSLock 是显示链表,有一个指向后续节点的属性。
  • 将获取锁的线程状态借助节点保存,每个线程都有一份独立的节点。

多线程并发访问资源

锁状态的分类

在 Java 中,syschronized 关键字设置了四种状态,分别为无锁、偏向锁、轻量级锁和重量级锁。

无锁

无锁状态集没有对资源进行锁定,任何线程都可以对同一个资源进行访问,但只有其中一个线程能够修改资源。无锁的特点就是在循环内修改操作,它会 i 不断的修改共享资源直至修改成功并退出,CAS 的原理和应用就是无锁的实现。

偏向锁

大多数情况下,锁不仅不存在多线程竞争,还存在锁由同一线程多次获取的情况,偏向锁就是在这种情况下诞生的,偏向锁比无锁多了线程 ID 和 epoch,下面我们来看看偏向锁的获取流程。

关于 epoch:它作为偏差有效性的时间戳。

轻量级锁

轻量级锁是指当前锁是偏向锁的时候,资源被另外的线程所访问,那么偏向锁会升级为轻量级锁,其他的线程会通过自旋的形式获取锁,不会去阻塞,从而提高性能。

下面我们来看看流程图:

重量级锁

当线程没有使用 CAS 成功获取到锁,就会自旋一会,再次尝试获取,如果多次自旋到达上限后还没有获取到锁,那么轻量级锁会升级为重量级锁。

锁的公平性与非公平性

在并发环境中,多个线程需要对同一资源进行访问,同一时刻只能由一个线程获取到锁并进行访问,就像大家去食堂排队打饭,先到达食堂的拥有最先买饭的权利,对于正常的排队的人来说,没有人插队,每个人都等待排队打饭的机会,这种方式就是公平的,这种锁也叫做公平锁,假如我们再排队的时候有人插队且没有人制止他,这种方式就是不公平的也叫非公平锁,在 Java 中,我们一般通过 ReetrantLock 来实现锁的公平性。废话不多说,上代码。

 package com.zhangpan;

import java.util.concurrent.locks.ReentrantLock;

public class demo01 {

    public static void main(String[] args) {

        MyFairLock myFairLock = new MyFairLock();

        Runnable runnable = ()->{
            System.out.println(Thread.currentThread().getName()+"启动。。");
            myFairLock.fairLock();

        };

        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {

            threads[i] = new Thread(runnable);
        }

        for (int i = 0; i < 10; i++) {

            threads[i].start();
        }
    }
}
  

那么如何实现非公平锁呢,只需要将上面代码改一个地方:

 private ReentrantLock lock = new ReentrantLock(false);
  

根据锁是否重入区分

可重入锁

可重入锁又叫递归锁,是指同一个线程再外层获取锁的时候,再进入该线程的内层方法自动获取锁,不会应为之前已经获取过还没释放而阻塞。在 Java 中 ReentrantLock 和 synchronized 都是可重入锁,另外还有一点就是可重入锁可以避免死锁。

 public class demo02 {

    private synchronized void eat(){

        System.out.println("eating.....");
        drink();

    }

    private synchronized void drink(){

        System.out.println("drinking.....");
    }
}
  

不可重入锁

如果 synchronized 是不可重入的,则调用 drink() 方法的时候必须将锁去掉,不然会导致死锁。

Static 关键字、String Stringbuilder、Strigbuilder 深入理解

static 关键字

1.static 修饰变量

static 关键字修饰的变量被称之为静态变量,他表示的是全局静态,注意的是 static 关键字修饰变量只能定义在类中,不能定义在任何方法中。

可以看到 static 定义在方法中出现了报错。

2.static 修饰方法

static 修饰的方法被称之为静态方法,且可以在没有创建任何对象的情况下仅仅通过类本身来调用 static 方法,这个也是 static 的主要用途。废话不多说,上代码。

 public class demo02 {


    static void staticMethod(){

        System.out.println("staticMethod is invoke");
    }

    public static void main(String[] args) {

        demo02.staticMethod();
    }

}
  

大家注意的一点的是,static 修饰的方法不能调用非静态方法,非静态方法内部可以调用静态方法。

3.static 修饰代码块

static 关键字可以用来修饰代码块,在类中,代码块有两种,一种是{}代码块,一种是 static{}代码块,static 修饰的代码块被称之为静态代码块,类中可以有多个代码块且可以放在类的任何位置,且静态代码块在类中只运行一次,可以用来进行数据库驱动初始化等操作。

 public class demo03 {

    static {

        System.out.println("static block...");
    }

    static void staticMethod(){

        System.out.println("static method...");
    }

    public static void main(String[] args) {
        demo03.staticMethod();
    }
}
  

4.static 修饰 j 静态内部类

静态内部类就是用 static 修饰的内部类,静态内部类可以包含静态成员,也可以包含非静态成员,但是在非静态内部类中不能声明静态成员,下面我们看一段代码。

 package com.zhangpan;

public class demo04 {
    private int a = 100;
    private static int b = 200;

    static class staticClass{
        public static int c = 300;
        public int d = 400;

        public static void print(){

            //静态内部类不能访问外部类成员
            //System.out.println("a--->"+a);

            //静态内部类可以访问外部类成员
            System.out.println("b--->"+b);
        }
    }

    public static void main(String[] args) {

        staticClass sc = new staticClass();
        sc.print();
    }
}
  

4.static 静态导包

静态导包就是使用 import static 来导入某个类或者某个包的静态方法或者变量。

 import static java.lang.Integer.*;

public class demo05 {

    public static void main(String[] args) {

        System.out.println(MAX_VALUE);
        System.out.println(toHexString(666));
    }
}
  

static 深入使用

  • static 所修饰的属性和方法都是属于类的,不会属于任何对象,调用方式都是类名.属性名/方法名。
  • static 修饰的变量都存储在方法区中。
  • static 的生命周期与类的生命周期相同,随类的加载而创建,随类的销毁而销毁。
  • 声明为 static 类型的变量不能被序列化,因为 static 修饰的变量保存在方法区中,而只有堆内存才能被序列化。
  • 在开发中 static 常用来做日志打印,通常将 Logger 对象声明为 static 变量,这样可以减少堆内存资源的占用。
  • static 可以用来做单例模式,使用 static 关键字可以保证 singleton 变量是静态的。

String

String 表示的是 Java 中的字符串,日常开发中常使用”“双引号包围表示都是字符串实例,String 类其实是使用 char[]来保存字符串的,下面我们看下源码。

 public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */    private final char value[];

....
  

可以发现 String 对象是由 final 修饰的,一旦使用 final 修饰就不能被继承、方法不能被重写,属性不能被修改,大家看上面的代码不知道发现没有,String 没有继承任何接口,但是实现了三个接口分别是 Serializable、Comparable、CharSequence 接口。

  • Serializable:这个序列化接口仅用于标识序列化语义。
  • Comparable:实现了 Comparable 的接口可以比较两个对象的大小。
  • CharSequence:字符串序列接口,CharSequence 是一个可读的序列值,提供了 length()、charAt(int index)、subSequence(int start,int end)等接口。

String 可以通过许多程序创建:

另外 String 还提供了很多方法:

  • charAt:判断 String 对象值是否相等
  • indexof:用于字符串检索
  • substring:用于对字符串进行截取
  • concat:用于字符串拼接,且比”+”效率高
  • replace:用于字符串替换
  • match:正则表达式的字符串匹配
  • contains:是否包含指定字符串
  • split:字符串分割
  • join:字符串拼接
  • trim:去掉多余的空格
  • toChatArray:将 String 对象转为字符串数组
  • valueof:将对象转换为字符串

StringBuffer

StringBuffer 对象表示一个可变的字符串数列,当一个 StringBuffer 被创建后,我们可以通过 StringBuffer 进行字符串的拼接,截取等操作,下面我们看一个例子。

         StringBuffer a = new StringBuffer("aaa");
        a.append("bbb");
        System.out.println(a);
  

另外还注意一点的是 StringBuffer 是线程安全的,大家可以看下源代码:

     @Override
    public synchronized StringBuffer append(Object obj) {
        toStringCache = null;
        super.append(String.valueOf(obj));
        return this;
    }

    @Override
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }

    public synchronized StringBuffer append(StringBuffer sb) {
        toStringCache = null;
        super.append(sb);
        return this;
    }
  

StringBuffer 在字符串拼接上使用 synchronized 关键字加锁,从而保证线程安全性。

StringBuilder

StringBuilder 类表示一个可变的字符序列,和 StringBuffer 就几乎是一样的,但是一个非线程安全的容器,一般适用于单线程的场景中的字符串拼接操作。

 public final class StringBuilder
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence{}
  

每当我们使用 String s = “aaa”的时候,就会创建一个 StringBuilder

Comparable 、Comparator 排序接口回顾

**Comparable **

Comparable 是一个排序接口,且次接口只有一个排序方法 compareTo(T o),且此方法支持任意类型的参数比较,当一个类实现了 Comparable 比较器,就意味着它本身支持排序,下面我来看一段代码。

 package com.zhangpan;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

public class demo06 {
    public static void main(String[] args) {
        List<Student> studentList = Arrays.asList(new Student("zhangsan",90),
                new Student("lisi",95),
                new Student("wangwu",88),
                new Student("zhoaqi",94)
                );

        //排序前
        System.out.println(studentList);
        Collections.sort(studentList);

        //排序后
        System.out.println(studentList);

        for (Student student:studentList){

            System.out.println(student.equals(new Student("lisi",95)));
        }
    }
}

class Student implements Comparable<Student>{

    String name;
    int record;

    public Student(String name, int record) {
        this.name = name;
        this.record = record;
    }

    public boolean equals(Student student){
        return name.equals(student.name) && record == student.record;
    }

    @Override
    public int compareTo(Student o) {
        return this.name.compareTo(o.name);
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getRecord() {
        return record;
    }

    public void setRecord(int record) {
        this.record = record;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", record=" + record +
                '}';
    }
}
  

Comparator

Comparator 作用和 Comparable 功能类似,它相当于一个比较器,也是使用 Collections.sort() 和 Arrays.sort() 来进行排序 不同于 Comparable,比较器可以任选的允许比较 null 参数,同时保持要求等价的关系。

Comparator 比较器方法:

 int compare(T o1,T o2);
  

下面我们来看一段代码:

 package com.zhangpan;

import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

class AsComparator implements Comparator<Student>{

    @Override
    public int compare(Student o1, Student o2) {
        return o1.getRecord() - o2.getRecord();
    }
}

public class demo07 {

    public static void main(String[] args) {

        List<Student> studentList = Arrays.asList(new Student("zhangsan",90),
                new Student("lisi",95),
                new Student("wangwu",88),
                new Student("zhoaqi",94)
        );

        //1.实现外部接口进行排序
        Collections.sort(studentList, new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                return o1.getRecord() - o2.getRecord();
            }
        });

        System.out.println(studentList);
    }
}
  

Comparable 和 Comparator 的对比

  • Comparable 更像自然排序,Comparator 更像是定制排序。
  • 对于普通数据类型,由于默认都实现了 Compatable 接口,实现了 compareTo 接口,我们可以直接使用,对于自定义类,我们可以新建 Comparator 接口,使用特定的 Comparator 实现进行比较。

理解强引用、弱引用、软引用、幻想引用相关概念

关于这块其实是很多概念性的东西:

  • 强引用 就是我们常见的创建对象实例的时候就是 new 过程,只要它还指向对象就表明它还活则,垃圾回收器就不会回收,当其对象赋值为 null 时才能被回收。
  • 软引用 用来描述一些还有用但是非必须的对象,且垃圾回收器会尽可能的保存软引用的对象,但是如果发生内存溢出错误的时候,会回收软引用对象,如果回收软引用的对象对象还不够分配,会直接抛出 OOM。
  • 弱引用 主要用来构建一种没有特定约束关系,可以用来做缓存。
  • 幻想引用 不能通过它访问对象,且当对象被 finalize 后会出发一些清理机制。

下面我们来看一张图:

Reference

所有的引用都是 java.lang.ref.Reference 的子类,它里面有一个 get() 方法,返回引用对象。除了幻想引用外,软引用和弱引用都是可以得到对象的,且这些引用可以认为拯救,变为强引用。另外因为 Reference 对象于垃圾回收密切配合实现,该类不能被直接子类化。

文章来源:智云一二三科技

文章标题:Java 核心基础知识总结(下)

文章地址:https://www.zhihuclub.com/184659.shtml

关于作者: 智云科技

热门文章

发表回复

您的电子邮箱地址不会被公开。

网站地图