Java中的集合&多线程的理解

Java中的集合

1、List、Set和Queue

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DwpgIu9t-1587300088779)(file:///C:\Users\ADMINI~1\AppData\Local\Temp\ksohtml3108\wps1.png)]

图中的绿色的虚线代表实现,绿色实线代表接口之间的继承,蓝色实线代表类之间的继承。

(1)list:

我们用的比较多List包括ArrayList和LinkedList

这两者的区别也很明显,从其名称上就可以看出。

/**
 * List
 *     有索引,有序(插入元素的先后顺序),允许重复的元素
 *      常用的List实现类:ArrayList、LinkedList
 *      ArrayList: 底层基于数据结构,查询效率高,增删效率低
 *      LinkedList: 底层基于链表结构,增删效率高,查询效率低,LinkedList中有很多方法可以操作首位元素
 *ArrayList
 *     ArrayList是线程不安全的,尽量声明为局部变量,在方法或者代码块中使用
 *     如果想在成员变量中定义ArrayList,要考虑到线程安全问题,建议使用
 *     CopyOnWriteArrayList或者调用Collections.synchronizedList()方法将ArrayList转换为一个线程安全的List
 *
 *     CopyOnWriteArrayList 通过“写时复制”保证高并发下的线程安全问题
 *     Collections.synchronizedList()是创建一个新的List,线程安全,底层通过同步锁来保证线程安全
 *
 *      扩展: CopyOnWriteArrayList “写时复制”
 *
 *LinkedList
 *      链表结构,有很多首尾元素操作的方法
 *      使用LinkedList可以模拟栈(先进后出)和队列(先进先出)这两种数据结构
 *
 *ArrayList和LinkedList的选择:
 *      查询多:ArrayList
 *      增删多:LinkedList
 *
 */
public class ListDemo {

    public static void main(String[] args) {

        MyStack<String> myStack = new MyStack<>();
        myStack.add("a");
        myStack.add("b");

        System.out.println(myStack.pop());
        System.out.println(myStack);
    }
    static class MyStack<T>{

        private LinkedList<T> stack;

        public MyStack(){
            this.stack = new LinkedList<>();
        }
        
        /**
         * 返回第一个元素 - 栈顶
         * @return
         */
        public T pop(){
            return stack.pollLast();
        }
        
        /**
         * 添加元素 - 添加到栈顶 - 链表的末尾
         */
        public void add(T t){
            stack.add(t);
        }
        /**
         * 重写tostring方法
         */ 
        public String toString(){
            return stack.toString();
        }
    }
}
①ArrayList:

底层基于数组结构,查询效率高,增删效率低

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0qpAG4Ap-1587300088781)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200419191625326.png)]

②LinkedList

底层基于链表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mN4q8TyC-1587300088783)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200419191728903.png)]

(2)Set:

Set与List的主要区别是Set是不允许元素重复的,而List则可以允许元素重复的。

/**
 * Set:
 *      没有索引,无序(插入顺序),自然排序或者定制排序规则,元素不重复(定制去重规则)
 *      常用的Set:HashSet、TreeSet
 *
 * HashSet:
 *      排序规则:根据Hash值进行排序
 *      去重规则:先判断hashCode,再判断equals,这两个都相等,则判定为相同元素
 *              如果hashcode不同,则认定为不同元素
 *              如果hashcode相同,再判断equals,如果equals返回true,表示为相同元素,如果equals返回false,则表示为不同元素
 *
 * TreeSet:二叉树可以减少比较次数
 *      排序规则:  自然排序,比较器
 *      去重规则:  根据排序规则返回0则为重复元素
 *
 *      TreeSet中的元素必须是可以比较的
 *      要么在创建TreeSet的时候指定了比较器
 *      要么元素本身就可以比较
 *
 * Iterable 实现了这个借口的集合都可以使用foreach
 *
 *      我们可以使用Iterator对元素进行遍历
 *      如果你想在元素遍历过程中对元素进行删除操作,我们需要使用Iterator,不要for循环
 *
 */
public class SetDemo {

    public static void main(String[] args) {

       Set<Student> students = new HashSet<>();
       students.add(new Student("张三",20));
       students.add(new Student("张三",20));
		
       System.out.println(students);


       TreeSet<Teacher> teachers = new TreeSet<>();
       teachers.add(new Teacher("李老师",18));
       teachers.add(new Teacher("树老师",19));

       System.out.println(teachers);
    }
}
①HashSet

底层是Hash表

public class Student {

    private String name;
    private Integer age;

    /**
     * 先根据hashcode方法判断,如果hashcode返回false,再判断下面的equals方法
     */
    @Override
    public int hashCode() {
        System.out.println("判断hashCode");
        return this.age;
    }
    
     /**
     * 再根据equals方法进行判断
     */
    @Override
    public boolean equals(Object obj) {
        System.out.println("判断equals");
        Student st = (Student) obj;
        return st.getName().equals(this.name);
    }

    public Student(String name, Integer age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
    
	/**
	 * getter&setter方法...
	 */
}

②TreeSet

底层是二叉树,减少比较次数

如果元素放入TreeSet中,要么元素可以自然排序(实现Comparable接口,重写

compareTo方法),要么在创建TreeSet的时候指定比较器(实现Comparator接

口,重写compare方法)

/**
 * 实现Comparable接口,重写compareTo方法
 */
public class Teacher implements Comparable<Teacher>{

    private String name;

    private Integer age;

    /**
     * 如果当前对象小于比较的对象,则返回负整数
     * 如果当前对象大于比较的对象,则返回正整数
     * 如果当前对象等于比较的对象,则返回0
     * 我们以年龄来排序
     * @param other
     * @return
     */
    @Override
    public int compareTo(Teacher other) {
        return this.age - other.getAge();
    }

    /**
     * setter&getter&构造方法...
	 */
}

/**
 * TreeSet的时候指定比较器 (实现Comparator接口,重写compare方法)
 */
public class SetDemo {
    public static void main(String[] args) {
		TreeSet<Student> students = new TreeSet<>((s1, s2) -> s1.getAge()-s2.getAge());
        students.add(new Student("张三",20));
        students.add(new Student("李四",20));

        System.out.println(students);
    }
}
 
(3)Queue:

一般可以直接使用LinkedList完成,从上述类图也可以看出,LinkedList继承自Deque,所以LinkedList具有双端队列的功能。PriorityQueue的特点是为每个元素提供一个优先级,优先级高的元素会优先出队列。

2、Map
(1)HashMap

jdk1.7之前,只有链表结构,没有二叉树

高并发情况下会导致HashMap链表成环,造成死循环,数据无法插入

https://www.cnblogs.com/lonelyJay/p/9726187.html

①HashMap底层原理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o95012uX-1587300088788)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200419193451464.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tazapA8B-1587300088790)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200419193503466.png)]

put方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2ZtH8jaa-1587300088792)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200419193544321.png)]

get方法

先根据key运算hash,然后根据hash和数组的长度运算某个数组的索引

去Node<>[]数组中获取指定索引的First Node,如果该节点的First Node是一个TreeNode,那么根据二叉树的规则寻找对应的key-value的Node

如果该节点的First Node不是TreeNode,则根据链表以此向后寻找,直到直到对应的key-value,返回value

属性:

// 默认的初始容量是16 
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //16 

// 当桶(bucket)上的结点数大于这个值时会转成红黑树 
static final int TREEIFY_THRESHOLD = 8; //(jdk1.8以后才有) 

// 当桶(bucket)上的结点数小于这个值时树转链表 
static final int UNTREEIFY_THRESHOLD = 6; 

// 存储元素的数组,总是2的幂次倍 
transient Node<k,v>[] table;

transient关键字:

序列化的时候忽略这个成员变量

②遍历方式
/**
 * Map:
 *      key,value形式存储的
 *      常用的实现类:HashMap
 *      HashMap是线程不安全的,可以使用Hashtable代替
 *      Hashtable使用的是sychronized将整个map加锁,效率低
 *      可以选择使用ConcurrentHashMap,使用的是分段锁的形式,在保证线程安全的前提下,又可以调高操作的效率
 *
 * Map的遍历方式有两种:
 *      (1)调用keySet获取所有的key,然后遍历所有的key再获取value
 *      (2)调用entrySet方法,获取key和value组成的Entry集合,遍历每一个Entry,调用getkey和getvalue获取entry中的key和value
 *      建议使用第二种方式,效率高
 *
 * ConcurrentHashMap 底层原理:为什么技能保证线程安全,又能保证操作效率?
 *
 */
public class MapDemo {

    public static void main(String[] args) {

        Map<String,String> map = new HashMap<>();
        map.put("name1","张三");
        map.put("name2","李四");


        //两种方式 - keySet()方法获取key组成的set集合
        //先取key,再取value,效率低
        for (String key : map.keySet()) {
            String value = map.get(key);
            System.out.println(key+":"+value);
        }

        System.out.println("=======================================");


        //第二种遍历方式,entryset,获取一组key,value的Entry的Set集合
        //建议使用这种方式,一次性把key和value取出,效率高
        for (Map.Entry<String, String> entry : map.entrySet()) {
            String key = entry.getKey();
            String value = entry.getValue();
            System.out.println(key+":"+value);
        }
    }
}
(2)HashTable

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I5RZzAhi-1587300088793)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200419194259565.png)]

(3)ConcurrentHashMap

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9k2rHGoI-1587300088795)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200419194425008.png)]

3、算法复杂度
(1)时间复杂度
①时间频度

一个算法执行所耗费的时间,从理论上是不能算出来的,必须上机运行测试才能知道。但我们不可能也没有必要对每个算法都上机测试,只需知道哪个算法花费的时间多,哪个算法花费的时间少就可以了。并且一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为T(n)。

②分类

按数量级递增排列,常见的时间复杂度有:

  • 常数阶O(1)

  • 对数阶O(log2n)

  • 线性阶O(n)

  • 线性对数阶O(nlog2n)

  • 平方阶O(n2)

  • 立方阶O(n3)

  • k次方阶O(nk)

  • 指数阶O(2n) 。

随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低。

③eg

要在 hash 表中找到一个元素就是 O(1)

要在无序数组中找到一个元素就是 O(n)

访问数组的第 n 个元素是 O(1)

访问链表的第 n 个元素是 O(n)

如果实现中没有循环就是 O(1)

如果实现中有一个循环就是 O(n)

(2)空间复杂度

与时间复杂度类似,空间复杂度是指算法在计算机内执行时所需存储空间的度量。

  • 记作: S(n)=O(f(n))

我们一般所讨论的是除正常占用内存开销外的辅助存储单元规模。

多线程

1、进程与线程
(1)进程

进程是资源(CPU、内存等)分配的基本单位,它是程序执行时的一个实例。程序运行时系统就会创建一个进程,并为它分配资源,然后把该进程放入进程就绪队列,进程调度器选中它的时候就会为它分配CPU时间,程序开始真正运行.

(2)线程

线程是程序执行时的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位,一个进程可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。同样多线程也可以实现并发操作,每个请求分配一个线程来处理。

进程是资源分配的最小单位,线程是程序执行的最小单位。

一个进程最少包含一个主线程。

2、实现多线程的三种方式
(1)继承Thread类:
public class ThreadDemo {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();//start方法启动线程

        for (int i = 0; i < 20; i++) {
            try {
                Thread.sleep(i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            if(i==10){
                myThread.interrupt();
            }

            System.out.println("main:"+i);
        }


    }

}

class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            //在业务代码中通过中断状态判断是否结束线程
            if(Thread.currentThread().isInterrupted()){
                break;
            }
            try {
                Thread.sleep(i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("MyThread:"+i);
        }
    }
}
(2)实现Runnable接口:

需要实现 run() 方法。

通过 Thread 调用 start() 方法来启动线程。

public class RunnableDemo {

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

        for (int i = 0; i < 20; i++) {
            try {
                Thread.sleep(i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("main:"+i);
        }
    }
}

class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            try {
                Thread.sleep(i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("MyRunnable:"+i);
        }
    }
}
(3)实现Callbal接口:

与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class CallableDemo {
    
    public static void main(String[] args) {
        MyCallable myCallable = new MyCallable();
        /**
         * 返回值通过FutrueTask进行封装
         */
        FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
        Thread thread = new Thread(futureTask);
        thread.start();

        for (int i=0; i < 20; i++) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("main:"+i);
        }

        try {
            //主线程中获取其他线程的返回值,那么使用callable
            Integer result = futureTask.get(); //callable接口重写call方法的返回值
            System.out.println(result);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

/**
 * 线程第三种实现方式 - callable接口,有返回值
 */
class MyCallable implements Callable<Integer>{

    @Override
    public Integer call() throws Exception {
        int i = 0;
        for (; i < 20; i++) {
            try {
                Thread.sleep(i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("MyCallable:"+i);
        }
        return i;
    }
}
(4)选择实现线程的方法

最好选择实现接口的方法,因为继承Thread只能继承一个,实现可以多实现.

如果需要返回值就选择实现CallBal接口.

3、一个线程的生命周期

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VM8Toeyh-1587300088798)(file:///C:\Users\ADMINI~1\AppData\Local\Temp\ksohtml3108\wps2.jpg)]

(1) 方法意思
  • yield方法:使当前线程从执行状态变为就绪状态。

  • sleep方法:强制当前正在执行的线程休眠,当睡眠时间到期,则返回到可运行状态。

​ 不会放弃锁资源

  • join方法:通常用于在main()主线程内,等待其它线程完成再结束main()主线程,不会放弃锁资源(守护线程)deamon:

    守护线程(deamon)是程序运行时在后台提供服务的线程,并不属于程序中不可或缺的部分。当所有非后台线程结束时,程序也就终止,同时会杀死所有后台线程。

  • main() 属于非后台线程。

使用 setDaemon() 方法将一个线程设置为后台线程。

(2) 线程的状态
  • 新建:刚刚new出来线程对象

  • 就绪:调用start方法之后进入就绪状态,等待获取cpu资源执行

  • 运行:获取cpu执行

  • 死亡:run结束或者异常抛出

  • 阻塞:sleep、wait、synchronized、io阻塞

如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:

① 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。

② 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。

③ 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。

(3) wait和sleep区别

sleep是Thread的方法,可以设置睡眠时间,时间到达后自动唤醒,进入就绪状态,

sleep不释放锁资源

wait是Object的方法,需要等待notify唤醒,wait会释放锁资源

(4) 扩展:

产生线程阻塞原因:

  1. 调用 Thread.sleep() 方法进入休眠状态;

  2. 通过 wait() 使线程挂起,直到线程得到 notify() 或 notifyAll() 消息(或者 java.util.concurrent 类库中等价的 signal() 或 signalAll() 消息;

  3. 等待某个 I/O 的完成;

  4. 试图在某个对象上调用其同步控制方法,但是对象锁不可用,因为另一个线程已经获得了这个锁。

线程中断:

使用中断机制即可终止阻塞的线程。

使用 interrupt() 方法来中断某个线程,它会设置线程的中断状态。Object.wait(), Thread.join() 和 Thread.sleep() 三种方法在收到中断请求的时候会清除中断状态,并抛出 InterruptedException。

应当捕获这个 InterruptedException 异常,从而做一些清理资源的操作。

1.不可中断的阻塞

不能中断 I/O 阻塞和 synchronized 锁阻塞。

2. Executor 的中断操作

Executor 避免对 Thread 对象的直接操作,但是使用 interrupt() 方法必须持有 Thread 对象。Executor 使用 shutdownNow() 方法来中断它里面的所有线程,shutdownNow() 方法会发送 interrupt() 调用给所有线程。

如果只想中断一个线程,那么使用 Executor 的 submit() 而不是 executor() 来启动线程,就可以持有线程的上下文。submit() 将返回一个泛型 Futrue,可以在它之上调用 cancel(),如果将 true 传递给 cancel(),那么它将会发送 interrupt() 调用给特定的线程。

3. 检查中断

通过中断的方法来终止线程,需要线程进入阻塞状态才能终止。如果编写的 run() 方法循环条件为 true,但是该线程不发生阻塞,那么线程就永远无法终止。

interrupt() 方法会设置中断状态,可以通过 interrupted() 方法来检查中断状,从而判断一个线程是否已经被中断。

interrupted() 方法在检查完中断状态之后会清除中断状态,这样做是为了确保一次中断操作只会产生一次影响。

4 如何中断线程的执行?

(1)设置退出标志(boolean类型的变量),使线程正常退出,也就是当run()方法完成后线程终止。

(2)使用 interrupt() 方法中断线程。

(3)使用 stop 方法强行终止线程(不推荐使用,Thread.stop, Thread.suspend, Thread.resume 和Runtime.runFinalizersOnExit 这些终止线程运行的方法已经被废弃,使用它们是极端不安全的!)

如果强制让线程停止有可能使一些清理性的工作得不到完成。另外一个情况就是对锁定的对象进行了解锁,导致数据得不到同步的处理,可能出现数据不一致的问题。

注意:

线程中断只是一个状态,不会真正中断线程,我们通常可以在线程业务代码中判断这个状态做出相关操作

4、线程之间的协作
(1)线程同步

给定一个进程内的所有线程,都共享同一存储空间,这样有好处又有坏处。这些线程就可以共享数据,非常有用。不过,在两个线程同时修改某一资源时,这也会造成一些问题。Java 提供了同步机制,以控制对共享资源的互斥访问。

① Synchronized关键字

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

  • 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的范围是大括号 { } 括起来的代码,作用的对象是调用这个代码块的对象,锁是synchronized关键词后的对象;

  • 修饰一个方法,被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象;

  • 修饰一个静态的方法,其作用的范围是整个静态方法,作用的对象是这个类的所有对象;

  • 修饰一个类,其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。

(2)Synchronized的作用主要有三个:

  • 确保线程互斥的访问同步代码

  • 保证共享变量的修改能够及时可见 可见性

  • 有效解决重排序问题。(指令重排序)

(3)Synchronized 原理 - 被 JVM控制,我们开发人员是无法干涉的

​ 在编译的字节码中加入了两条指令来进行代码的同步。

monitorenter :每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

  • 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。

  • 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.

  • 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权。

monitorexit

执行monitorexit的线程必须是objectref所对应的monitor的所有者。

指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

通过这两段描述,我们应该能很清楚的看出Synchronized的实现原理,Synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。

② Lock

lock是一个类,主要有以下几个方法:

lock():获取锁,如果锁被暂用则一直等待

unlock():释放锁,在finally中释放锁,防止死锁

tryLock(): 注意返回类型是boolean,如果获取锁的时候锁被占用就返回false,否则返回true

tryLock(long time, TimeUnit unit):比起tryLock()就是给了一个时间期限,保证等待参数时间

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qKTgSjRX-1587300088800)(file:///C:\Users\ADMINI~1\AppData\Local\Temp\ksohtml3108\wps3.jpg)]

Lock底层其实是CAS乐观锁的体现

eg:

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

public class LockDemo {

    public static void main(String[] args) {

        MyRunable myRunable = new MyRunable();

        Thread t1 = new Thread(myRunable,"线程1");
        Thread t2 = new Thread(myRunable,"线程2");

        t1.start();
        t2.start();

    }

}

class MyRunable implements Runnable{

    //同一把锁,Lock默认是非公平锁,可以通过构造方法创建一个公平锁
    Lock lock = new ReentrantLock(true);


    @Override
    public void run() {
        test1();
    }

    private void test(){

        try{
            //使用lock实现同步锁
            lock.lock();
            for (int i = 0; i < 20; i++) {
                try {
                    Thread.sleep(i);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //一定在finally中释放锁资源
            lock.unlock();
        }
    }

    private void test1(){
        try{
            if(lock.tryLock()){
                for (int i = 0; i < 20; i++) {
                    try {
                        Thread.sleep(i);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+":"+i);
                }
            }else{
                //获取不到锁,不会一直等待,做其他使用
                System.out.println(Thread.currentThread().getName()+":我不等了,我走了...................");
            }

        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }
    }
}
③ synchronized 与 lock 的区别

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5TXvZ2V7-1587300088801)(file:///C:\Users\ADMINI~1\AppData\Local\Temp\ksohtml3108\wps4.jpg)]

  • Lock的加锁和解锁都是由java代码实现的,而synchronize的加锁和解锁的过程是由JVM管理的。

  • synchronized能锁住类、方法和代码块,而Lock是块范围内的。

  • Lock能提高多个线程读操作的效率;

  • Lock:Lock实现和synchronized不一样,后者是一种悲观锁,它胆子很小,它很怕有人和它抢吃的,所以它每次吃东西前都把自己关起来。而Lock底层其实是CAS乐观锁的体现,它无所谓,别人抢了它吃的,它重新去拿吃的就好啦,所以它很乐观。底层主要靠volatile和CAS乐观锁操作实现的。

(2)线程通信

一个线程用来生产,一个线程用来消费

wait() notify() notifyAll()

​ 它们都属于 Object 的一部分,而不属于 Thread。而 sleep() 是 Thread 的静态方法;

① wait()

​ 会在等待时将线程挂起,而不是忙等待,并且只有在 notify() 或者 notifyAll() 到达时才唤醒。

​ sleep() 和 yield() 并没有释放锁,但是 wait() 会释放锁。

实际上,只有在同步控制方法或同步控制块里才能调用 wait() 、notify() 和 notifyAll()。

② notify()

​ 该方法用来通知那些可能等待该对象的对象锁的其他线程。如果有多个线程等待,则线程规划器任意挑选出其中一个 wait()状态的线程来发出通知,并使它等待获取该对象的对象锁。

③ notifyAll()

​ 使所有原来在该对象上 wait 的线程统统退出 wait 的状态(即全部被唤醒,不再等待 notify 或 notifyAll,但由于此时还没有获取到该对象锁,因此还不能继续往下执行),变成等待获取该对象上的锁。

④ Eg

容器 (锁的对象)

public class Conainter {

    private int num = 0;

    /**
     * 生产
     */
    public synchronized void product(){

        num++;
        System.out.println("生产了一个包子,当前"+num+"个包子");
        try {
            if(num>=5){
                //盘子放满了
                wait();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //唤醒其他线程去消费
        notify();


    }

    /**
     * 消费
     */
    public synchronized void consume(){

        num--;
        System.out.println("消费了一个包子,当前"+num+"个包子");
        try {
            if(num<=0){
                //盘子空了
                wait();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //唤醒其他线程去生产
        notify();
    }
}

生产者

import java.util.Random;

/**
 * 生产者
 */
public class Producter implements Runnable {

    private Conainter conainter;

    public Producter(Conainter conainter){
        this.conainter = conainter;
    }

    @Override
    public void run() {
        try {
            //死循环
            for(;;){
                Thread.sleep(new Random().nextInt(1000));
                conainter.product();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

消费者

import java.util.Random;
/**
 * 消费者
 */
public class Consumer implements Runnable {

    private Conainter conainter;

    public Consumer(Conainter conainter){
        this.conainter = conainter;
    }

    @Override
    public void run() {
        try {
            //死循环
            for(;;){
                Thread.sleep(new Random().nextInt(1000));
                conainter.consume();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

测试类

public class Demo {

    public static void main(String[] args) {
        Conainter conainter = new Conainter();
        Thread product = new Thread(new Producter(conainter));
        Thread consumer = new Thread(new Consumer(conainter));

        product.start();
        consumer.start();
    }
}

测试结果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2JlUPaht-1587300088804)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200419201449437.png)]

(3)线程安全
① 概念

当多个线程访问一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替执行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获得正确的结果,那这个对象是线程安全的。

多个线程调用同一个对象,如果这个对象的操作结果是正确的,那么我们就说这个对象是线程安全的。

② 线程安全的实现方法
  • 互斥同步

互斥同步(Mutual Exclusion&Synchronization)是常见的一种并发正确性保障手段。同步是指在多个线程并发访问共享数据时,保证共享数据在同一个时刻只被一个(或者是一些,使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区(Critical Section)、互斥量(Mutex)和信号量(Semaphore)都是主要的互斥实现方式。因此,在这 4 个字里面,互斥是因,同步是果;互斥是方法,同步是目的。在 Java 中,最基本的互斥同步手段就是 synchronized 关键字。

除了 synchronized 之外,我们还可以使用 java.util.concurrent(下文称 J.U.C)包中的重入锁(ReentrantLock)来实现同步,在基本用法上,ReentrantLock 与 synchronized 很相似,他们都具备一样的线程重入特性,只是代码写法上有点区别,一个表现为 API 层面的互斥锁(lock() 和 unlock() 方法配合 try/finally 语句块来完成),另一个表现为原生语法层面的互斥锁。不过,相比 synchronized,ReentrantLock 增加了一些高级功能,主要有以下 3 项:等待可中断、可实现公平锁,以及锁可以绑定多个条件。

等待可中断是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助。

公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁;而非公平锁则不保证这一点,在锁被释放时,任何一个等待锁的线程都有机会获得锁。synchronized 中的锁是非公平的,ReentrantLock 默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。

锁绑定多个条件是指一个 ReentrantLock 对象可以同时绑定多个 Condition 对象,而在 synchronized 中,锁对象的 wait() 和 notify() 或 notifyAll() 方法可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而 ReentrantLock 则无须这样做,只需要多次调用 newCondition() 方法即可。

  • 非阻塞同步 乐观锁实现(时间戳或者版本号)了解

互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步(Blocking Synchronization)。从处理问题的方式上说,互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施(例如加锁),那就肯定会出现问题,无论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。随着硬件指令集的发展,我们有了另外一个选择:基于冲突检测的乐观并发策略,通俗地说,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断地重试,直到成功为止),这种乐观的并发策略的许多实现都不需要把线程挂起,因此这种同步操作称为非阻塞同步(Non-Blocking Synchronization)。

  • 无同步方案

要保证线程安全,并不是一定就要进行同步,两者没有因果关系。同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性,因此会有一些代码天生就是线程安全的。

可重入代码 (Reentrant Code):这种代码也叫做纯代码(Pure Code),可以在代码执行的任何时刻中断它,转而去执行另外一段代码(包括递归调用它本身),而在控制权返回后,原来的程序不会出现任何错误。

线程本地存储 (Thread Local Storage):如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。比如Web 交互模型中的“一个请求对应一个服务器线程”(Thread-per-Request)的处理方式。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-76q0iwhi-1587300088805)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200419203124550.png)]

Eg :

public class ThreadLocalTest {


}

class Container{

    /**
     * 同一个成员变量要在多个线程中使用,并且相互不干涉,使用ThreadLocal,
     * 如果多个线程必须指向同一个副本,使用线程同步(锁)
     */
    private ThreadLocal<Integer> num = new ThreadLocal<>();

    public void add(){
        //get()获取当前线程的副本,set方法设置当前线程的副本值
        num.set(num.get()+1);
    }
}

Java 语言中,如果一个变量要被多线程访问,可以使用 volatile 关键字声明它为“易变的”;如果一个变量要被某个线程独享,Java 中就没有类似 C++中 __declspec(thread)这样的关键字,不过还是可以通过 java.lang.ThreadLocal 类来实现线程本地存储的功能。

(4)锁:
① 锁的类型和锁的优化

在 java 中锁的实现主要有两类:内部锁 synchronized(对象内置的monitor锁)和显示锁java.util.concurrent.locks.Lock。

在 java.util.concurrent.locks 包中有很多Lock的实现类,常用的有 ReentrantLock 和ReadWriteLock,其实现都依赖 java.util.concurrent.AbstractQueuedSynchronizer 类。

② 锁的一些概念
  • 可重入锁:

指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响,执行对象中所有同步方法不用再次获得锁。避免了频繁的持有释放操作,这样既提升了效率,又避免了死锁。

  • 自旋锁:

所谓“自旋”,就是让线程去执行一个无意义的循环,循环结束后再去重新竞争锁,如果竞争不到继续循环,循环过程中线程会一直处于running状态,但是基于JVM的线程调度,会让出时间片,所以其他线程依旧有申请锁和释放锁的机会。自旋锁省去了阻塞锁的时间空间(队列的维护等)开销,但是长时间自旋就变成了“忙式等待”,忙式等待显然还不如阻塞锁。所以自旋的次数一般控制在一个范围内,例如10,100等,在超出这个范围后,自旋锁会升级为阻塞锁。

  • 公平锁:

按等待获取锁的线程的等待时间进行获取,等待时间长的具有优先获取锁权利

  • 读写锁:

对资源读取和写入的时候拆分为2部分处理,读的时候可以多线程一起读,写的时候必须同步地写。

  • 独占锁:

是一种悲观锁,synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。

  • 乐观锁:(CAS)

每次不加锁,假设没有冲突去完成某项操作,如果因为冲突失败就重试,直到成功为止。

③ synchronized的优化

JDK1.6后对synchronized做了优化

  • 偏向锁(无锁):

大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得。偏向锁的目的是在某个线程获得锁之后(线程的id会记录在对象的Mark Word中),消除这个线程锁重入(CAS)的开销,看起来让这个线程得到了偏护。

  • 轻量级锁(CAS):

轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;轻量级锁的意图是在没有多线程竞争的情况下,通过CAS操作尝试将MarkWord更新为指向LockRecord的指针,减少了使用重量级锁的系统互斥量产生的性能消耗。

  • 重量级锁:

虚拟机使用CAS操作尝试将MarkWord更新为指向LockRecord的指针,如果更新成功表示线程就拥有该对象的锁;如果失败,会检查MarkWord是否指向当前线程的栈帧,如果是,表示当前线程已经拥有这个锁;如果不是,说明这个锁被其他线程抢占,此时膨胀为重量级锁。

5、ThreadLocal
(1)概念

很多地方叫做线程本地变量,也有些地方叫做线程本地存储。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

ThreadLocal在每个线程中对该变量会创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。

Synchronized 用于线程间的数据共享,而 ThreadLocal 则用于线程间的数据隔离。

初始容量16,负载因子2/3,解决冲突的方法是再hash法,也就是:在当前hash的基础上再自增一个常量进行哈希。

(2)原理

ThreadLocalMap中用于存储数据的entry定义,使用了弱引用,可能造成内存泄漏。

当线程没有结束,但是ThreadLocal对象已经被回收,则可能导致线程中存在ThreadLocalMap的键值对,造成内存泄露。(ThreadLocal被回收,ThreadLocal关联的线程共享变量还存在)。解决办法:

(1)使用完线程共享变量后,显式调用ThreadLocalMap.remove方法清除线程共享变量;

(2)ThreadLocal定义为private static,这样ThreadLocal的弱引用问题则不存在0了。

ThreadLocal类提供的几个方法:

public T get() { }

public void set(T value) { }

public void remove() { }

protected T initialValue() { }

get()方法是用来获取ThreadLocal在当前线程中保存的变量副本,set()用来设置当前线程中变量的副本,remove()用来移除当前线程中变量的副本,initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法。

每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。

初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。

然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。

ThreadLocal配置多数据源

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值