Java并发编程实战(一)
一、 创建多线程的几种方式
(1)继承Thread类并重写run()方法
实例代码:
public class ThreadDemo2 extends Thread{ public ThreadDemo2(String name) { super(name); } @Override public void run(){ while (!interrupted()){ System.out.println(getName() + "--线程执行了。。。"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { ThreadDemo2 t1 = new ThreadDemo2("Thread-001"); ThreadDemo2 t2 = new ThreadDemo2("Thread-002"); t1.start(); t2.start(); t1.interrupt(); //中断线程t1 } }
(2)实现Runnable接口的run()方法
实例代码:
public class ThreadDemo1 implements Runnable{ @Override public synchronized void run() { while (true){ System.out.println("自定义线程执行了。。。"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { ThreadDemo1 th = new ThreadDemo1(); //初始化状态 Thread thread = new Thread(th); //创建线程,并指定线程任务 //启动线程 thread.start(); while (true){ System.out.println("主线程执行了。。。"); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } }
(3)匿名内部类的方式
实例代码:
public class ThreadDemo4 { public static void main(String[] args) { //方式一 new Thread(){ public void run(){ System.out.println(getName() + "线程执行了。。。"); }; }.start(); //方式二 new Thread(new Runnable() { @Override public void run() { System.out.println("Runnable线程执行了"); } }).start(); //方式三,这里体现了多态,子类将会覆盖父类的方法 new Thread(new Runnable() { @Override public void run() { System.out.println("Thread-001执行了..."); } }){ public void run(){ System.out.println("Thread-002执行了..."); }; }.start(); } }
(4)带返回值的线程(实现Callable接口)
实例代码:
import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; public class ThreadDemo5 implements Callable<String> { @Override public String call() throws Exception { System.out.println("线程正在处理任务中...."); Integer sum = 0; for(int i=1;i<=100;i++){ sum += i; } Thread.sleep(3000); return "任务处理成功, sum= " + sum; } public static void main(String[] args) { ThreadDemo5 th5 = new ThreadDemo5(); //创建异步任务 FutureTask<String> futureTask = new FutureTask<>(th5); //创建Thread对象 Thread thread = new Thread(futureTask); //启动线程 thread.start(); //获取返回结果 try { //等待任务执行完毕,并返回结果 String result = futureTask.get(); System.out.println(result); }catch (Exception e){ e.printStackTrace(); } } }
(5)使用定时器创建线程(Timer)
实例代码:
import java.util.Timer; import java.util.TimerTask; public class ThreadDemo6 { public static void main(String[] args) { //jdk自带的定时器 Timer timer = new Timer(); //调用启动定时任务,该定时任务每隔4秒执行一次,延迟时间为0 //TimerTask 是一个抽象类,它实现了Runnable接口 timer.schedule(new TimerTask() { Integer sum = 0; @Override public void run() { //实现定时任务 sum += 10; System.out.println("timertask is run; sum = " + sum); } },0, 4000); } }
(6)线程池的实现
实例代码:
import java.util.concurrent.Executor; import java.util.concurrent.Executors; public class ThreadDemo7{ public static void main(String[] args) { //创建线程池,并执行线程的个数为10 Executor threadPool = Executors.newFixedThreadPool(10); //执行线程 for (int i=0;i<100;i++){ threadPool.execute(new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName() + "执行了..."); } }); } } }
(7)Lambda表达式的实现
实例代码:
import java.util.Arrays; import java.util.List; public class ThreadDemo9 { public Integer add(List<Integer> values){ return values.parallelStream().mapToInt(a -> a * 2).sum(); //并发流,求和 } public Integer add1(List<Integer> values){ values.stream().forEach(System.out :: println); return 0; } public static void main(String[] args) { List<Integer> values = Arrays.asList(10,20,30,40,50,60); Integer result = new ThreadDemo9().add(values); System.out.println("计算结果为:" + result); Integer result1 = new ThreadDemo9().add1(values); System.out.println("计算结果为:" + result1); } }
二、线程带来的问题
(一)线程安全性问题
(1)多线程环境下
(2)多个线程共享一个资源
(3)对资源进行读写(非原子性)操作
在以上三个条件下,将会出现线程安全问题
(二)活跃性问题
(1)死锁: 线程之间占用资源,且都不愿意释放资源,就会造成死锁
(2)饥饿:
高优先级吞噬所有低优先级的CPU时间片,优先级低的线程一直抢不到资源,导致饿死;
线程被永久堵塞在一个等待进入同步块的状态;
处于等待状态的线程永远不被唤醒;
如何避免饥饿问题?
a. 设置合理的优先级
b. 使用锁来代替synchronized
(3)活锁
windows下cmd中执行命令:
(1)jconsole 来查看Java线程监视器
(2)javap -verbose xxx.class 查看Java字节码文件
(三)性能问题
三、线程优先级
在Java中线程优先级:1 到 10, 默认为5。值越大,表示优先级越高。
实例代码:
public class ThreadDemo1 { public static void main(String[] args) { //创建Thread对象 Thread thread1 = new Thread(new Target()); Thread thread2 = new Thread(new Target()); Thread thread3 = new Thread(new Target()); Thread thread4 = new Thread(new Target()); Thread thread5 = new Thread(new Target()); //设置优先级 thread1.setPriority(10); //可以看到线程1的优先级很高,几乎都是它在运行 thread2.setPriority(3); thread3.setPriority(6); thread4.setPriority(1); thread5.setPriority(8); //启动线程 thread1.start(); thread2.start(); thread3.start(); thread4.start(); thread5.start(); } }
四、 Java中的加锁机制
在 Java 中主要2种加锁机制:
(1)synchronized 关键字
(2)java.util.concurrent.Lock (Lock是一个接口,ReentrantLock是该接口一个很常用的实现)
这两种机制的底层原理存在一定的差别
synchronized 关键字通过一对字节码指令 monitorenter/monitorexit 实现, 这对指令被 JVM 规范所描述。
java.util.concurrent.Lock 通过 Java 代码搭配sun.misc.Unsafe 中的本地调用实现的。
五、synchronized原理
synchronized:是Java语言的一个关键字,它可以用来修饰方法或者代码块,在JVM层面上
(1)在获取锁的线程执行完同步代码时,释放锁
(2)当线程执行发生异常时,JVM会让线程释放锁
(3)在成功执行完成或抛出异常时,虚拟机会自动释放线程占有的锁
缺点:
(1)synchronized不能响应中断
(2)同一时刻不管是读还是写操作,都只能有一个线程对共享资源操作,其他线程只能等待
(3)锁的释放由虚拟机完成,不需要人工干预
被synchronized修饰的方法,方法的内部相当于是一个原子性操作,可以保证线程的安全性操作。
1.内置锁:在Java中,每个对象都可以用作同步的锁,这些锁就被称之为内置锁。
2.互斥锁:线程在进入同步代码块之前会自动获取锁,并且在退出同步代码块时会自动释放锁,当某个线程请求一个由其他线程持有的锁时,发出请求的线程就会阻塞。
互斥锁其实提供了一种原子操作,让所有线程以串行的方式执行同步代码块。
可重入性:
某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功(重复获取),这就叫互斥锁的可重入性。“重入”意味着获取锁的操作的粒度是“线程”,而不是调用,也就是说一个线程中可以针对同一个锁多次获取,而不需要阻塞等待。
synchronized关键字修饰的同步代码块,是一种互斥锁,是原子性操作。
类方法用synchronized关键字修饰,和synchronized(this)一样,获取的是当前类对象的互斥锁。
修饰普通方法:synchronized 放在普通方法上,内置锁就是当前类的实例
修饰静态方法:synchronized修饰静态方法,内置锁是当前的Class字节码对象,也就是方法所在类的Class字节码对象
修饰代码块:在任意对象上获取的是该对象的同步锁。
任何对象都可以作为锁,那么锁信息又存在对象的什么地方呢?
锁信息保存在对象头中。
对象头中的信息:
(1)Mark Word: 对象头,它存放对象的Hash值,以及锁信息。
偏向锁的Mark Word还记录线程id, Epoch, 对象的分代年龄信息, 是否是偏向锁, 锁标志位(锁标志位为“01”状态,是否为偏向锁为“0”)
(2)Class Metadata Address
(3)Array Length
六、 synchronized底层优化
锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。
随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。
JDK 1.6中默认是开启偏向锁和轻量级锁的,我们也可以通过-XX:-UseBiasedLocking来禁用偏向锁。
参考资源:https://blog.csdn.net/lengxiao1993/article/details/81568130
参考资源:https://www.cnblogs.com/paddix/p/5405678.html
synchronized 代码块是由一对 monitorenter/moniterexit 字节码指令实现, monitor 是其同步实现的基础, Java SE1.6 为了改善性能, 使得 JVM 会根据竞争情况,使用如下 3 种不同的锁机制:
偏向锁(Biased Lock )
轻量级锁( Lightweight Lock)
重量级锁(Heavyweight Lock)
上述这三种机制的切换是根据竞争激烈程度进行的,
在几乎无竞争的条件下, 会使用偏向锁,
在轻度竞争的条件下, 会由偏向锁升级为轻量级锁,
在重度竞争的情况下, 会升级到重量级锁。
无锁状态: 锁标志位为“01”状态
轻量级锁状态:锁标志位为“00”状态
重量级锁状态:锁标志位为“10”状态
注意 JVM 提供了关闭偏向锁的机制, JVM 启动命令指定如下参数即可:
-XX:-UseBiasedLocking
(一)偏向锁
(1)每次获取锁和释放锁会浪费资源;
(2)很多情况下,竞争锁不是由多个线程,而是由一个线程在使用;
以上情况的出现,有了偏向锁。
只有一个线程在访问同步代码块的场景时,使用偏向锁。
引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径,因为轻量级锁的获取及释放依赖多次CAS原子指令,而偏向锁只需要在置换ThreadID的时候依赖一次CAS原子指令(由于一旦出现多线程竞争的情况就必须撤销偏向锁,所以偏向锁的撤销操作的性能损耗必须小于节省下来的CAS原子指令的性能消耗)。
偏向锁是在只有一个线程执行同步块时进一步提高性能。
(二)轻量级锁
“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁。
轻量级锁-自旋:
(1)自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。
(2)适应性自旋(Adaptive Spinning): 当线程在获取轻量级锁的过程中执行CAS操作失败时,是要通过自旋来获取重量级锁的。问题在于,自旋是需要消耗CPU的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费CPU资源。解决这个问题最简单的办法就是指定自旋的次数,例如让其循环10次,如果还没获取到锁就进入阻塞状态。但是JDK采用了更聪明的方式——适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。
(三)重量级锁
Synchronized是通过对象内部的一个叫做监视器锁(monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为“重量级锁”。JDK中对Synchronized做的种种优化,其核心都是为了减少这种重量级锁的使用。JDK1.6以后,为了减少获得锁和释放锁所带来的性能消耗,提高性能,引入了“轻量级锁”和“偏向锁”。
Synchronized就是重量级锁。
关于Java并发编程相关的知识,后续将会不断更新!
相关文章