分类 标签 存档 社区 博客 友链 GitHub 订阅 搜索

Java - 多线程

482 浏览

ZERO

    持续更新 请关注:https://zorkelvll.cn/blogs/zorkelvll/articles/2018/12/20/1545316493765

背景

     本文主要是记录在学习 Java 多线程过程中的一些知识点备忘!

core 关键点

  • 核心线程池、队列、拒绝策略

Final、问题

1、线程池的原理是什么?线程池的 6 个参数分别是什么含义?线程池的几个状态?

1.1 原理 - 即线程池的处理流程:

imagepng

1.2 线程池参数

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, RejectedExecutionHandler handler)
  • corePoolSize: 线程池的基本大小
  • maximumPoolSize: 线程池最大线程大小
  • keepAliveTime & unit: 线程空闲后的存活时间及时间单位
  • workQueue: 用于存放任务的阻塞队列
  • handler: 当队列和最大线程池都满了之后的饱和策略

1.3 线程池状态

  • RUNNING: 运行状态,即可以接受任务执行队列里的任务
  • SHUTDOWN: 指调用了 shutdown() 方法,不再接受新任务了,但是队列的任务得执行完毕
  • STOP: 指调用了 shutdownNow() 方法,不再接受新任务了,同时抛弃阻塞队列里的所有任务并中断所有正在执行的任务
  • TIDYING: 所有任务都执行完毕,在调用 shutdown() 和 shutdownNow() 方法时都会尝试更新这个状态
  • TERMINATED: 终止状态,当执行 terminated() 后会更新为这个状态

imagepng

2、ThreadLocal 的原理?其中的 Map 中 key 和 value 分别是什么?

2.1 ThreadLocal 是一个数据结构,类似于 HashMap 存储 KV 键值对,但是 ThreadLocal 只能保存一个,且各个线程的数据互不干扰

2.2 如何保证各个线程的数据互不干扰的?每个线程均有一个 ThreadLocalMap 数据结构,当执行 set 方法时其值是保存在当前线程threadlocals 变量中;当执行 get 方法时,从当前线程的 threadlocals 变量中获取

=> 因此,当前线程之间不会相互干扰!

2.3 ThreadLocalMap

ThreadLocalMap(jdk1.7) 也是一个类似于 HashMap 的数据结构,初始化一个大小 16 的 Entry 数组,Entry 对象用以保存每一个 KV 键值对,且 key 永远都是 ThreadLocal 对象!也即,通过 ThreadLocal 对象的 set 方法,然后把 ThreadLocal 对象自己当做 key,存入 ThreadLocalMap 中去

ThreadLocalMap 中的 Entry 继承 WeakReference,与 HashMap 不同的是其 Entry 没有 next 字段,也即不存在链表

hash 冲突

内存泄漏:当使用 ThreadLocal 保存一个 value 时,会在 ThreadLocalMap 中的数组插入一个 Entry 对象,按理说 key-value 都应该以强引用保存在 Entry 对象中,但在 ThreadLocalMap 的实现中,key 被保存到了 WeakReference 对象中;这就导致了一个问题,ThreadLocal 在没有外部强引用时,发生 GC 时会被回收,如果创建 ThreadLocal 的线程一直持续运行,那么这个 Entry 对象中的 value 就有可能一直得不到回收,发生内存泄露。

避免内存泄漏:有内存泄露的隐患,自然有应对的策略,在调用 ThreadLocal 的 get()、set() 可能会清除 ThreadLocalMap 中 key 为 null 的 Entry 对象,这样对应的 value 就没有 GC Roots 可达了,下次 GC 的时候就可以被回收,当然如果调用 remove 方法,肯定会删除对应的 Entry 对象。如果使用 ThreadLocal 的 set 方法之后,没有显示的调用 remove 方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完 ThreadLocal 之后,记得调用 remove 方法。

四、JDK 并发容器 - JUC(java.util.concurrent 包)

  • ConcurrentHashMap: 线程安全的 HashMap
  • CopyOnWriteArrayList: 线程安全的 List, 在读多写少的场合中性能非常好,远远好于 Vector
  • ConcurrentLinkedQueue: 高效的并发队列,使用链表实现;可以看作是一个线程安全的 LinkedList,是一个非阻塞队列
  • BlockingQueue: 一个接口,JDK 内部通过链表、数组等方式实现该接口;表示阻塞队列,非常适合用于作为数据共享的通道
  • ConcurrentSkipListMap: 跳表的实现,是一个 Map,使用跳表的数据结构进行快速查找

三、synchronized 关键字 同步语句块

  • synchronized 声明方法的缺点
  • synchronized(this) 同步代码块的使用
  • synchronized(object) 代码块间使用
  • synchronized 代码块间的同步性:其他线程执行对象 synchronized 同步方法和 synchronized(this) 代码块时呈现同步效果;如果两个线程使用了同一个 “对象监视器”,运行结果同步,否则不同步
  • 静态同步 synchronized 方法与 synchronized(class) 代码块:synchronized 关键字加到 static 方法和 synchronized(class) 代码块上都是给 Class 类上锁,而 synchronized 关键字加到非 static 静态方法上都是给对象上锁
  • 数据类型 String 的常量池属性: 在 JVM 中具有 String 常量池缓存的功能

二、synchronized 关键字 同步方法

  • 变量安全性:如果两个线程同时操作对象中的实例变量,则会出现 “非线程安全”,解决办法就是在方法前加上 synchronized 关键词即可
  • 多个对象多个锁
  • synchronized 方法与锁对象:synchronzied 取得的锁都是对象锁,而不是一段代码或方法当做锁
  • 脏读:发生脏读的情况是在读取实例变量时,此值已经被其他线程更改过
  • synchronized 锁重入:“可重入锁” 概念是 “自己可以再次获取自己的内部锁”,另外可重入锁也支持在父子类继承的环境中
  • 同步不具有继承性

一、java 多线程基础概念

  • 进程 VS 线程:基本概念,什么是多线程?
  • 如何使用多线程:继承 Thread 类,实现 Runnable 接口,使用线程池(juc 包中的 Executors 类)
  • 实例变量和线程安全:不共享数据的情况,共享数据的情况
  • 常用方法:currentThread(),getId(),getName(),getPriority(),isAlive(),sleep(long millis),interrupt(),interrupted() 和 isInterrupted(),setName(String name),isDaemon(),setDaemon(boolean on),join(),yield(),setPriority(int newPriority)
  • 如何停止一个线程:使用 interrupt()方法;使用 return 停止线程;已弃用的 [(1)stop() 不安全;(2)stop(Throwable obj)不安全;(3)suspend()+resume()死锁];
  • 线程的优先级
  • java 多线程分类:多线程分类,如何设置守护?

20181229

1、程序 VS 进程 VS 线程

  • 程序:含有指令和数据的文件,被存储在磁盘或者其他的数据存储设备中,也即程序 = 静态的代码
  • 进程:是程序的一次执行过程,是系统运行程序的基本单位,是动态的!系统运行一个程序即是一个进程从创建、运行到消亡的过程。简单地说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着;同时,每个进程还占有某些系统资源如 CPU 时间、内存空间、文件、输入输出设备的使用权等等;也即,当程序在执行时,将会被操作系统载入内存中
  • 线程:是进程划分成的更小的运行单位。线程与进程最大的不同在于基本上各进程是独立的,而各线程不一定,因为同一进程中的线程极有可能相互影响;从另一个角度说,进程属于操作系统的范畴,主要是在同一时间段内,可以同时执行一个以上的程序,而线程则是在同一个程序内几乎同时执行一个以上的程序段

线程上下文切换比进程上下文切换要快很多

  • 进程切换时,涉及到当前进程的 CPU 环境的保存和新被调度运行进程的 CPU 环境的设置
  • 线程切换仅需要保存和设置少量的寄存器内存,不涉及存储管理方面的操作

2、线程有哪些基本状态?这些状态是如何定义的?

  • 新建 new:新创建了一个线程对象
  • 可运行 runnable:线程对象被创建后,其他线程如 main 线程调用了该该对象的 start 方法;该状态的线程位于可运行线程池中,等待被线程调度选中,获取 cpu 的使用权
  • 运行 running:可运行状态 runnable 的线程获得了 cpu 时间 timeslice,执行程序代码
  • 阻塞 block:阻塞状态是指线程因为某种原因放弃了 CPU 使用权,也即让出了 cpu timeslice,暂时停止运行,直到线程进入可运行 runnable 状态,才有机会再次获得 cpu timeslice 转到运行 running 状态。阻塞情况分三种:

(1)、等待阻塞:运行 running 的线程执行 o.wait() 方法,JVM 会把该线程放入等待队列 waiting queue 中 (2)、同步阻塞:运行 running 的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池 lock pool 中 (3)、其他阻塞:运行 running 的线程执行 Thread.sleep(long ms) 或 t.join() 方法时,或者发生了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep() 状态超时或超时 join() 等待线程终止或超时、或者 I/O 处理完毕后

  • 死亡 dead:线程 run、main 方法执行结束,或因异常退出了 run 方法,则该线程结束生命周期;死亡的线程不可再次复生。

20181222-2

1、synchronized 声明方法的缺点

答曰:使用 synchronized 关键字声明方法是有很大弊端的,比如两个线程 A 和 B,一个线程 A 调用同步方法获得锁,那么另一个线程 B 就需要等待 A 执行完,但是如果说 A 执行的是一个很费时间的任务的话这样就会很耗时

如下是一个暴露 synchronized 声明方法的缺点示例,以及如何通过 synchronzied 同步语句块去解决这样的问题

public class Task {

    private String getData1;
    private String getData2;

    public synchronized void doLongTimeTask() {
        try {
            System.out.println("begin task");
            Thread.sleep(3000);
            getData1 = "长时间处理任务后从远程返回的值1 threadName="
                    + Thread.currentThread().getName();
            getData2 = "长时间处理任务后从远程返回的值2 threadName="
                    + Thread.currentThread().getName();
            System.out.println(getData1);
            System.out.println(getData2);
            System.out.println("end task");
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

public class CommonUtils {

    public static long beginTime1;
    public static long endTime1;

    public static long beginTime2;
    public static long endTime2;
}


public class Aokay1Thread extends Thread {
    private Task task;
    public Aokay1Thread(Task task) {
        super();
        this.task = task;
    }
    @Override
    public void run() {
        super.run();
        CommonUtils.beginTime1 = System.currentTimeMillis();
        task.doLongTimeTask();
        CommonUtils.endTime1 = System.currentTimeMillis();
    }
}


public class Aokay2Thread extends Thread {
    private Task task;
    public Aokay2Thread(Task task) {
        super();
        this.task = task;
    }
    @Override
    public void run() {
        super.run();
        CommonUtils.beginTime2 = System.currentTimeMillis();
        task.doLongTimeTask();
        CommonUtils.endTime2 = System.currentTimeMillis();
    }
}


public class Run {

    public static void main(String[] args) {
        Task task = new Task();

        Aokay1Thread at1 = new Aokay1Thread(task);
        at1.start();

        Aokay2Thread at2 = new Aokay2Thread(task);
        at2.start();

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

        long beginTime = CommonUtils.beginTime1;
        if (CommonUtils.beginTime2 < CommonUtils.beginTime1) {
            beginTime = CommonUtils.beginTime2;
        }

        long endTime = CommonUtils.endTime1;
        if (CommonUtils.endTime2 > CommonUtils.endTime1) {
            endTime = CommonUtils.endTime2;
        }

        System.out.println("耗时:" + ((endTime - beginTime) / 1000));
    }
}

从运行时间上看(耗时约 6 秒,即第二个线程等待第一个线程执行完之后才执行),synchronized 方法问题很明显;可以使用 synchronized 同步块来解决这个问题,需要注意的是 synchronized 同步块的使用方式,使用不当并不会带来效率上的提升

2、synchronized(this) 同步代码块的使用

将上述的中的 Task 类代码进行如下修改:

public class Task {

    private String getData1;
    private String getData2;

    public void doLongTimeTask() {
        try {
            System.out.println("begin task");
            Thread.sleep(3000);

            String privateGetData1 = "长时间处理任务后从远程返回的值1 threadName="
                    + Thread.currentThread().getName();
            String privateGetData2 = "长时间处理任务后从远程返回的值2 threadName="
                    + Thread.currentThread().getName();

            //这个地方是核心的不同的地方!!!
            synchronized (this) {
                getData1 = privateGetData1;
                getData2 = privateGetData2;
            }

            System.out.println(getData1);
            System.out.println(getData2);
            System.out.println("end task");
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

从运行时间上看(耗时约 3 秒,即第二个线程并不需要等待第一个线程执行完,就可以执行)

=》当一个线程访问一个对象的 synchronized 同步代码块时,另一个线程仍然可以访问对象非 synchronized 同步代码块

3、synchronized(object) 代码块间使用

也即,两个或多个线程是否使用同一个 “对象监视器”,如果是使用同一个对象监视器则结果是同步的,否则运行结果就不是同步的了!代码示例如下:

public class AokayObject {
}

public class Service {

    public void testMethod1(AokayObject object) {
        synchronized (object) {
            try {
                System.out.println("testMethod1 ____getLock time="
                        + System.currentTimeMillis() + " run ThreadName="
                        + Thread.currentThread().getName());
                Thread.sleep(2000);
                System.out.println("testMethod1 releaseLock time="
                        + System.currentTimeMillis() + " run ThreadName="
                        + Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class Aokay1Thread extends Thread {

    private Service service;
    private AokayObject object;

    public Aokay1Thread(Service service, AokayObject object) {
        super();
        this.service = service;
        this.object = object;
    }

    @Override
    public void run() {
        super.run();
        service.testMethod1(object);
    }
}

public class Aokay2Thread extends Thread {
    private Service service;
    private AokayObject object;

    public Aokay2Thread(Service service, AokayObject object) {
        super();
        this.service = service;
        this.object = object;
    }

    @Override
    public void run() {
        super.run();
        service.testMethod1(object);
    }
}

public class Run1 {

    public static void main(String[] args) {
        Service service = new Service();
        //下面两个线程均使用同一个的该“对象监视器” =>结果是同步的
        AokayObject object = new AokayObject();

        Aokay1Thread a = new Aokay1Thread(service, object);
        a.setName("a");
        a.start();

        Aokay2Thread b = new Aokay2Thread(service, object);
        b.setName("b");
        b.start();
    }
}

public class Run2 {

    public static void main(String[] args) {
        Service service = new Service();
        //下面两个线程各自使用的不同的对象的“对象监视器” =>结果是不同步的
        AokayObject object1 = new AokayObject();
        AokayObject object2 = new AokayObject();

        Aokay1Thread a = new Aokay1Thread(service, object1);
        a.setName("a");
        a.start();

        Aokay2Thread b = new Aokay2Thread(service, object2);
        b.setName("b");
        b.start();
    }
}

4、synchronized 代码块间的同步性

当一个对象访问 synchronized(this)代码块时,其他线程对同一个对象中所有其他 synchronized(this)代码块代码的访问都将被阻塞,这说明 “synchronized(this) 代码块使用的是同一个对象监视器”,也即 synchronized(this)代码块也是锁定当前对象的

=>

  • 其他线程执行对象中 synchronized 同步方法和 synchronized(this) 代码块时呈现同步效果
  • 如果两个线程使用了同一个 “对象监视器” 则运行结果同步,否则不同步

5、静态同步 synchronized 方法与 synchronized(class) 代码块

synchronized 关键字加到 static 静态方法和 synchronized(class) 代码块都是给 Class 类上锁的,而 synchronized 关键字加到非 static 静态方法上则是给对象上锁的

public class Service {

    public static void printA() {
        synchronized (Service.class) {
            try {
                System.out.println(
                        "线程名称为:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "进入printA");
                Thread.sleep(3000);//该时间差用以标记是否同步
                System.out.println(
                        "线程名称为:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "离开printA");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    synchronized public static void printB() {
        System.out.println("线程名称为:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "进入printB");
        System.out.println("线程名称为:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "离开printB");
    }

    synchronized public void printC() {
        System.out.println("线程名称为:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "进入printC");
        System.out.println("线程名称为:" + Thread.currentThread().getName() + "在" + System.currentTimeMillis() + "离开printC");
    }
}


public class Aokay1Thread extends Thread {
    private Service service;
    public Aokay1Thread(Service service) {
        super();
        this.service = service;
    }
    @Override
    public void run() {
        service.printA();
    }
}

public class Aokay2Thread extends Thread {
    private Service service;
    public Aokay2Thread(Service service) {
        super();
        this.service = service;
    }
    @Override
    public void run() {
        service.printB();
    }
}

public class Aokay3Thread extends Thread {
    private Service service;
    public Aokay3Thread(Service service) {
        super();
        this.service = service;
    }
    @Override
    public void run() {
        service.printC();
    }
}

public class Run {
    public static void main(String[] args) {
        Service service = new Service();

        Aokay1Thread a = new Aokay1Thread(service);
        a.setName("A");
        a.start();

        Aokay2Thread b = new Aokay2Thread(service);
        b.setName("B");
        b.start();

        Aokay3Thread c = new Aokay3Thread(service);
        c.setName("C");
        c.start();
    }
}

=》运行是第一条先打印的 A,然后是两条 C, 然后再是一条 A 和两条 B

=》静态同步 synchronized 方法与 synchronized(class) 代码块持有的锁一样,都是 Class 锁,Class 锁对对象的所有实例均起作用!synchronized 关键字加到非 static 静态方法上持有的是对象锁

=》线程 A,B 和线程 C 持有的锁不一样,所以 A 和 B 运行同步,但是和 C 运行不同步

6、数据类型 String 的常量池属性

在 JVM 中具有 String 常量池缓存的功能

String s1 = "a"; 
String s2 = "a"; 
System.out.println(s1==s2);//true

上面打印结果是 true,为什么?

答曰:字符串常量池中的字符串只存在一份!即执行完第一行代码后,常量池中已存在 “a”,那么 s2 不会再在常量池中申请新的空间,而是直接把已经存在的字符串内存地址返回给 s2

=> 因此数据类型 String 的常量池属性,所有 synchronized(string) 在使用时某些情况会出现一些问题,比如两个线程运行

synchronized("abc"){}

//和

synchronized("abc"){}

修饰的方法时,这两个线程就会持有相同的锁,导致某一时刻只有一个线程能够运行!所有尽量不要使用 synchronized(string),而是使用 synchronized(object)

20181222-1

1、synchronized 关键字

synchronized 关键字也被称为重量级锁,在 JDK1.6 之后进行了主要包括 “减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁”,以及其他各种优化之后变得在某些情况下并不是重了

2、变量安全性

“非线程安全”问题存在于 “实例变量” 中,如果是方法内部的私有变量,则不存在 “非线程安全问题”,所以得到结果也就是“线程安全” 的了

如果两个线程同时操作对象中的实例变量,则会出现 “非线程安全”,解决办法就是在方法前加上 synchronized 关键字即可!

(可见 20181220-8 示例)

3、多个对象多个锁

public class HasSelfPrivateNum {

    private int num = 0;

    synchronized public void addI(String username) {
        try {
            if (username.equals("a")) {
                num = 100;
                System.out.println("a set over!");
                //如果去掉hread.sleep(2000),那么运行结果就会显示为同步的效果
                Thread.sleep(2000);
            } else {
                num = 200;
                System.out.println("b set over!");
            }
            System.out.println(username + " num=" + num);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

}


public class Aokay1Thread extends Thread {
    //定义了一个属于Aokay1Thread的私有对象HasSelfPrivateNum
    private HasSelfPrivateNum numRef;

    public Aokay1Thread(HasSelfPrivateNum numRef) {
        super();
        this.numRef = numRef;
    }

    @Override
    public void run() {
        super.run();
        numRef.addI("a");
    }

}


public class Aokay2Thread extends Thread {
    //定义了一个属于Aokay2Thread的私有对象HasSelfPrivateNum
    private HasSelfPrivateNum numRef;

    public Aokay2Thread(HasSelfPrivateNum numRef) {
        super();
        this.numRef = numRef;
    }

    @Override
    public void run() {
        super.run();
        numRef.addI("b");
    }

}


public class Run {

    public static void main(String[] args) {
        //创建了两个HasSelfPrivateNum对象
        HasSelfPrivateNum numRef1 = new HasSelfPrivateNum();
        HasSelfPrivateNum numRef2 = new HasSelfPrivateNum();

        Aokay1Thread at1 = new Aokay1Thread(numRef1);
        //开启线程at1且在该线程的run中的锁是对象numRef1的对象锁
        at1.start();

        Aokay2Thread at2 = new Aokay2Thread(numRef2);
        //开启线程at2且在该线程的run中的锁是对象numRef2的对象锁
        at2.start();

    }

}


执行结果是: a set over b set over b num=200 a num=100

两个线程 Aokay1Thread 和 Aokay2Thread 分别访问的是同一个类 HasSelfPrivateNum 的不同实例的相同名称的同步方法,但是效果却是异步执行。为什么会是这样的呢??

因为 synchronized 取得的锁都是对象锁即例子中 HasSelfPrivateNum 类某个实例对象的锁,而不是把一段代码或方法当做锁!在实际运行中,哪个线程先执行带 synchronized 关键字的方法,则哪个线程就持有该方法所属对象的锁 Lock, 那么其他线程只能呈等待状态,前提是多个线程访问的是同一个对象(本例子很显然上面是两个对象)

在上述例子中,是创建了两个 HasSelfPrivateNum 类对象,因此就会产生两个锁!当 Aokay1Thread 线程 run 方法中的引用 numRef 执行到 addI 方法中的 Thread.sleep(2000) 语句是,Aokay2Thread 就会乘机执行,所以才会导致上述执行结果(注意:a num=100 会在停顿了 2 秒才输出)

4、synchronzied 方法与锁对象

由 3 可知,synchronized 取得的锁是对象锁,而不是把一段代码或方法当做锁;如果多个线程访问的是同一个对象,哪个线程先执行到带 synchronized 关键字的方法,则哪个线程就持有该方法所属对象的锁 Lock,其他的线程只能呈等待状态!如果多个线程访问的是多个对象则不一定吗,因为多个对象会产生多个锁!

那么,当多个线程访问的是同一个对象中的非 synchronized 类型的方法会出现什么效果呢?

答曰:会异步调用非 synchronized 类型方法,解决办法就是在该方法前加上 synchronzied 关键字

5、脏读

脏读发生的情况就是在读取实例变量时,该值已经被其他线程更改过了!

public class PublicVar {

    public String username = "A";
    public String password = "AA";

    synchronized public void setValue(String username, String password) {
        try {
            this.username = username;
            Thread.sleep(5000);
            this.password = password;

            System.out.println("setValue method thread name="
                    + Thread.currentThread().getName() + " username="
                    + username + " password=" + password);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
     //该方法前加上synchronized关键字就同步了
     public void getValue() {
        System.out.println("getValue method thread name="
                + Thread.currentThread().getName() + " username=" + username
                + " password=" + password);
    }
}

public class Aokay1Thread extends Thread {

    private PublicVar publicVar;

    public Aokay1Thread(PublicVar publicVar) {
        super();
        this.publicVar = publicVar;
    }

    @Override
    public void run() {
        super.run();
        publicVar.setValue("B", "BB");
    }
}

public class Run {

    public static void main(String[] args) {
        try {
            PublicVar publicVarRef = new PublicVar();
            Aokay1Thread at1 = new Aokay1Thread(publicVarRef);
            at1.start();

            Thread.sleep(200);//打印结果受此值大小影响

            publicVarRef.getValue();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

运行结果是:

setValue method thread name=main username=B password=AA getValue method thread name=Thread-0 username=B password=BB

解决办法:在 getValue 方法前加上 synchronized 关键字即可

运行结果是: setValue method thread name=Thread-0 username=B password=BB getValue method thread name=main username=B password=BB

6、synchronized 锁重入

可重入锁:自己可以再次获取自己的内部锁!

比如一个线程获得了某个对象的锁,此时这个对象锁还没释放,当其再次想要获取这个对象的锁的时候也还是可以获取的,如果不可锁重入的话,就会造成死锁

public class Service {

    synchronized public void service1() {
        System.out.println("service1");
        service2();
    }

    synchronized public void service2() {
        System.out.println("service2");
        service3();
    }

    synchronized public void service3() {
        System.out.println("service3");
    }

}


public class AokayThread extends Thread {
    @Override
    public void run() {
        Service service = new Service();
        service.service1();
    }

}

public class Run {  
    public static void main(String[] args) { 
        AokayThread at = new AokayThread(); 
        at.start(); 
    } 
}

运行结果: service1 service2 service3

此外,可重入锁也支持在父子类继承的环境中

public class Main {

    public int i = 10;

    synchronized public void operateIMainMethod() {
        try {
            i--;
            System.out.println("main print i=" + i);
            Thread.sleep(100);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

public class Sub extends Main {

    synchronized public void operateISubMethod() {
        try {
            while (i > 0) {
                i--;
                System.out.println("sub print i=" + i);
                Thread.sleep(100);
                this.operateIMainMethod();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class AokayThread extends Thread {
    @Override
    public void run() {
        Sub sub = new Sub();
        sub.operateISubMethod();
    }
}

public class Run {  
    public static void main(String[] args) { 
        AokayThread at = new AokayThread(); 
        at.start(); 
    } 
}

运行结果:sub print i=9 main print i=8 …… sub print i=1 main print i=0

=>当存在父子类继承关系时,子类是完全可以通过 “可重入锁” 调用父类的同步方法!

另外,当出现异常时,其所持有的锁会自动释放

7、同步不具有继承性

如果父类有一个带有 synchronized 关键字的方法,子类继承并重写了这个方法

但是同步不能继承,所以还是需要在子类方法中添加 synchronized 关键字

20181221

1、多线中的常用方法

  • currentThread():返回对当前正在执行的线程对象的引用
  • getId():返回此线程的标识符
  • getName():返回此线程的名称
  • getPriority():返回此线程的优先级
  • isAlive():测试这个线程是否还处于活动状态,所谓活动状态就是指线程已经启动且尚未终止,线程处于正在运行或准备运行的状态
  • sleep(long millis):使当前正在执行的线程以指定的毫秒数 “休眠”(暂时停止执行),具体取决于系统定时器和调度程序的精度和准确性
  • interrupt():中断这个线程
  • interrupted() 和 isInterrupted():???interrupted 为测试当前线程是否已经是中断状态,执行后具有将状态标志清除为 false 的功能;isInterrupted 为测试线程 Thread 对相关是否已经是中断状态,但不清除状态标志
  • setName(String name):将此线程的名称更改为参数 name 的值
  • isDaemon():测试这个线程是否是守护线程
  • setDaemon(boolean on):将此线程标记为 daemon 线程或用户线程
  • join():在很多情况下,主线程生成并启动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是 “主线程需要等待子线程执行完成之后再结束,这个时候就需要用到 join() 方法了”;;join 的作用就是“等待该线程终止”,这里的该线程指的就是主线程等待子线程的终止,也就是在子线程调用了 join 方法后面的代码,只有等到子线程结束了才能执行
  • yield():作用是放弃当前的 CPU 资源,将它让给其他的任务去占用 CPU 时间!注意:放弃的时间不确定,可能一会就会重新获得 CPU 时间片
  • setPriority(int newPriority):更改此线程的优先级

2、interrupt() 和 return 两种方式停止线程

???代码编写??

3、线程的优先级

每个线程都具有各自的优先级,线程的优先级可以在程序中表明该线程的重要性,如果有很多线程处于就绪状态,系统会根据优先级来决定首先使哪个线程进入运行状态;但这并不意味着低优先级的线程得不到运行,而只是它运行的几率比较小,如垃圾回收机制线程的优先级就比较低(所以很多垃圾得不到及时的回收处理)

线程优先级具有继承特性:比如 A 线程启动 B 线程,则 B 线程的优先级和 A 是一样的 线程优先级具有随机特性:即线程优先级高的不一定每一次都先执行完

Thread 类中包含的成员变量代表了线程的某些优先级,如Thread.MIN_PRIORITY(常数 1)Thread.NORM_PRIORITY(常数 5)Thread.MAX_PRIORITY(常数 10);其中每个线程的优先级都在Thread.MIN_PRIORITY(常数 1)Thread.MAX_PRIORITY(常数 10)之间,在默认情况下优先级都是Thread.NORM_PRIORITY(常数 5)

4、java 多线程分类

  • 用户线程:运行在前台,执行具体的任务,如程序的主线程、连接网络的子线程等都是用户线程
  • 守护线程:

(1)运行在后台,为其他前台线程服务,也即守护线程是 JVM 中非守护线程的 “佣人”

(2)特点:一旦所有的用户线程均结束运行,则守护线程会随着 JVM 一起结束工作

(3)应用:数据库连接池中的检测线程,JVM 虚拟机启动后的检测线程

(4)最常见的守护线程:垃圾回收线程

  • 如何设置守护线程?通过调用 Thread 类的 setDaemon(true) 方法设置当前线程为守护线程,需要注意的是:(a)setDaemon(true) 必须在 start() 方法前执行,否则会抛出 IllegalThreadStateException 异常;(b)在守护线程中产生的新线程也是守护线程;(c)不是所有的任务都可以分配给守护线程来执行的,比如读写操作或者计算逻辑

  • ??守护线程相关的示例代码

20181220

1、进程 VS 线程

进程是程序的一次执行过程,是系统运行程序的基本单位(因此,进程是动态的);系统中运行一个程序即是一个进程从创建、运行到消亡的过程

线程是比进程更小的执行单位;一个进程在其执行过程中可以产生多个线程;与进程不同的是,同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程或者多个线程之间切换工作时,负担要比进程小得多,因此线程也被称为轻量级进程

多线程:多个线程同时运行(多核 CPU)或交替运行(单核 CPU,即顺序地交替执行)

多线程的必要性:开发高并发系统的基础,利用好多线程机制可以大大提高系统整体的并发能力和性能

为什么提倡多线程而不是多进程?:因为线程间的切换和调度的成本要远远小于进程间的切换和调度

2、同步 VS 异步

  • 同步和异步均用来形容一次方法调用

  • 同步:同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为

  • 异步:异步方法调用更像是一个消息传递,一旦开始,方法调用就会立即返回,调用者可以继续后续的操作

  • 关于异步的经典常用实现方式:使用消息队列;在不使用消息队列时,用户的请求数据是直接写入数据库的,因此在高并发情况下数据库压力剧增,导致响应速度变慢;而在使用消息队列之后,用户请求数据发送给消息队列之后立即返回,再由消息队列的消费者进程从消息队列中获取数据,异步写入数据库!

  • 由于消息队列服务器处理速度快于数据库,且消息队列比数据库有更好的伸缩性,因此响应速度会大幅改善!

3、并发 Concurrency VS 并行 Paralleism

并发与并行均可以表示两个或多个任务一起执行,但是偏重点不同:并行是真正意义上的的 “同时执行”,而并发对于单个 CPU 是多线程交替运行(并发),对于多个 CPU 是可以同时运行(并行);即多核时的并发 = 并行

4、高并发 HC(High Concurrency)

高并发是互联网分布式系统架构设计中必须考虑的一个因素,通常是指设计保证系统能够同时并行处理很多请求;高并发的一些常用指标有:响应时间 ResponseTime、吞吐量 Throughput、每秒查询率 QPS(Query Per Second)、并发用户数等

5、临界区

临界区即是用来表示一种公共资源或者说是共享数据,可以被多个线程使用;但是,每一次只能有一个线程可以使用它,一旦临界区资源被占用,其他线程想要使用这个资源,就必须等待;在并行程序中,临界区资源是保护的对象

6、阻塞 VS 非阻塞

非阻塞是指在不能立刻得到结果之前,该函数不会阻塞当前线程,而是会立刻返回;阻塞与之相反、

7、创建多线程的方式

(1)继承 Thread 类 【该方式实际开发过程中不使用】

public class AokayThread extends Thread {
  @Override
  public void run(){
    //super.run();
    System.out.println("This 
 is AokayThread");
  }
}

public class Aokay {
  public static void main(String[] args){
    AokayThread at = new AokayThread();
    at.start();
    System.out.println("Main主线程运行结束!!!");
  }
} 

=> 定义一个 Thread 的继承类 AokayThread=> 重写 run() 方法 => 在另一个线程如主线程 Main 中 new 一个该继承类,并使用 start() 方法开启该线程(执行该线程 run 方法中的内容)

=> 线程 AokayThread 在主线程 Main 中是一个子任务,且 CPU 是以不确定的方式或者说是随机的时间来调用该线程中的 run 方法的

(2)实现 Runnable 接口

相比较于 (1) 更加推荐实现 Runnable 接口方式开发多线程,这是因为 java 继承是单继承但是是可以实现多个接口的

public class AokayRunnable implements Runnable {
  @Override
  public void run(){
    System.out.println("This is AokayRunnable");
  }
}

public class Aokay {
  public static void main(String[] args) {
    Runnable ar = new AokayRunnale();
    Thread t = new Thread(ar);
    t.start();
    System.out.println("Main主线程运行结束!!!");
  }
}

=> 定义一个 Runnable 接口的实现类 AokayRunnable=> 重写 run() 方法 => 在另一个线程如主线程 Main 中首先 new 一个该实现类,然后再用该实现类 new 一个 Thread 类出来,最后使用 Thread 对象的 start() 方法开启该线程(执行该线程 run 方法中的内容)

(3)使用线程池

相比较于(1)和(2),使用线程池是最为推荐的一种方式,在阿里巴巴 java 开发手册中就明确强调到 “线程资源必须通过线程池提供,不允许在应用中自行显示创建线程”

具体示例代码将在线程池部分详细介绍

8、实例变量和线程安全

线程类中的实例变量针对其他线程可以有共享和不共享之分,例子如下:

  • 不共享数据的情况:多个线程之间不共享变量,线程安全的情况
public class AokayThread extends Thread {
  
  priavte int count = 5;

  public AokayThread(String name){
    super();
    this.setName(name);
  }

  @Override
  public void run(){
    super.run();
    while(count > 0){
      count--;
      System.out.println("由:" + AokayThread.currentThread().getName() + "计算,count=" + count);
    }
  }

  public static void main(String[] args){
    //区别主要是此处构造了三个Thread子类AokayThread的对象
    AokayThread a = new AokayThread("A");
    AokayThread b = new AokayThread("B");
    AokayThread c = new AokayThread("C");

    //直接用各个类AokayThrea的对象启动各个线程
    a.start();
    b.start();
    c.start();
  }
}

根据运行结果,是每个线程均是有一个属于自己的实例变量 count,它们之间互不影响!

  • 共享数据的情况:多个线程之间共享变量,线程不安全的情况
public class Aokay2Thread extends Thread{

  private int count = 5;

  @Override
  public void run(){
    super.run();
    //while(count > 0){
      count--;
      System.out.println("由:" + Aokay2Thread.currentThread().getName() + "计算,count=" + count);
    //}
  }

  public static void main(String[] args){
    //区别主要是此处仅构造了一个Thread子类Aokay2Thread的对象
    Aokay2Thread a2t = new Aokay2Thread();
    //再用Aokay2Thread对象a2t去构造五个Thread类对象
    Thread a = new Thread(a2t,"A");
    Thread b = new Thread(a2t,"B");
    Thread c = new Thread(a2t,"C");
    Thread d = new Thread(a2t,"D");
    Thread e = new Thread(a2t,"E");

    //然后用各个Thread类对象去启动各个线程
    a.start();
    b.start();
    c.start();
    d.start();
    e.start();
  }
}

根据运行结果,发现这里面可能会出现了错误,明明想要的是依次递减的结果,为什么呢?这是因为在大多数 jvm 中, count–分为以下三个步骤:(1)取得原有 count 值;(2)计算 count-1;(3)对 count 进行赋值

==》因此,多个线程同时访问就会出现问题(??具体如何分析一步步解释运行结果,可根据具体结果进行猜测逻辑??)

==》如何解决共享数据的线程安全问题?一是利用 synchronized 关键字(保证任意时刻只能有一个线程执行该方法),二是利用 AtomicInteger(即 JUC 中的 Atomic 原子类)!!注意,不能用 volatile 关键字,因为 volatile 关键字不能保证复合操作的原子性。

说明:本 JavaGuide 系列博客为来源于https://github.com/Snailclimb/JavaGuide 等学习网站或项目中的知识点,均为自己手打键盘系列且内容会根据继续学习情况而不断地调整和完善!

评论  
留下你的脚步
推荐阅读