Java并发编程的艺术[1]

时间:2022-07-22
本文章向大家介绍Java并发编程的艺术[1],主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

昨天阅读翻译了CompletableFuture的源码,目前百度,有道,基本是翻译效果一般,Google翻译比较准确,源码有很多注释,写个小测试类将其去掉,另外获得了《Java并发编程的艺术》PDF版,因为需要测试demo,就要转word,又找了个小测试类转成word,效果不错。参考《Java并发编程的艺术》

1.上下文切换

个人理解:CPU需要暂停当前任务,执行另一个任务,另一个任务完成后再执行当前任务,我们知道时钟中断导致cpu切换进程

原文:CPU通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。

2.多线程一定快吗?

书中demo

public class Question1 {
private static final long count = 10000L;
public static void main(String[] args) throws InterruptedException {
            concurrency();
            serial();
new CompletableFuture<>();
        }
private static void concurrency() throws InterruptedException {
long start = System.currentTimeMillis();
            Thread thread = new Thread(
            ()->{
int a = 0;
for (long i = 0; i < count; i++) {
                    a += 5;
                }
            });
            thread.start();
int b = 0;
for (long i = 0; i < count; i++) {
                b--;
            }
long time = System.currentTimeMillis() - start;
            thread.join();
            System.out.println("concurrency :" + time+"ms,b="+b);
        }
private static void serial() {
long start = System.currentTimeMillis();
int a = 0;
for (long i = 0; i < count; i++) {
                a += 5;
            }
int b = 0;
for (long i = 0; i < count; i++) {
                b--;
            }
long time = System.currentTimeMillis() - start;
            System.out.println("serial:" + time+"ms,b="+b+",a="+a);
        }
}

1.1.2 测试上下文切换次数和时长

下面我们来看看有什么工具可以度量上下文切换带来的消耗。

·使用Lmbench3[1]可以测量上下文切换的时长。

·使用vmstat可以测量上下文切换的次数。

下面是利用vmstat测量上下文切换次数的示例。

个人操作如下

vmstat(Virtual Memory Statistics 虚拟内存统计) 命令用来显示Linux系统虚拟内存状态,也可以报告关于进程、内存、I/O等系统整体运行状态

vmstat 1

CS(Content Switch)表示上下文切换的次数,从上面的测试结果中我们可以看到,上下文

每1秒切换1000多次。

示例1:vmstat 命令说明

[root@zhaokk opt]# vmstat
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
3  0      0 1201060   7204 234044    0    0     1     5    8   15  1  1 99  0  0

Procs(进程)

r:

运行队列中进程数量,这个值也可以判断是否需要增加CPU。(长期大于1)

b

等待IO的进程数量。

Memory(内存)

swpd

使用虚拟内存大小,如果swpd的值不为0,但是SI,SO的值长期为0,这种情况不会影响系统性能。

free

空闲物理内存大小。

buff

用作缓冲的内存大小。

cache

用作缓存的内存大小,如果cache的值大的时候,说明cache处的文件数多,如果频繁访问到的文件都能被cache处,那么磁盘的读IO bi会非常小。

Swap

si

每秒从交换区写到内存的大小,由磁盘调入内存。

so

每秒写入交换区的内存大小,由内存调入磁盘。

注意:内存够用的时候,这2个值都是0,如果这2个值长期大于0时,系统性能会受到影响,磁盘IO和CPU资源都会被消耗。有些朋友看到空闲内存(free)很少的或接近于0时,就认为内存不够用了,不能光看这一点,还要结合si和so,如果free很少,但是si和so也很少(大多时候是0),那么不用担心,系统性能这时不会受到影响的。因为linux总是先把内存用光

IO

bi

每秒读取的块数

bo

每秒写入的块数

注意:随机磁盘读写的时候,这2个值越大(如超出1024k),能看到CPU在IO等待的值也会越大。

system(系统)

in

每秒中断数,包括时钟中断。

cs

每秒上下文切换数。

注意:上面2个值越大,会看到由内核消耗的CPU时间会越大。

CPU(以百分比表示)

us

用户进程执行时间百分比(user time) us的值比较高时,说明用户进程消耗的CPU时间多,但是如果长期超50%的使用,那么我们就该考虑优化程序算法或者进行加速。

sy:

内核系统进程执行时间百分比(system time) sy的值高时,说明系统内核消耗的CPU资源多,这并不是良性表现,我们应该检查原因。

wa

IO等待时间百分比 wa的值高时,说明IO等待比较严重,这可能由于磁盘大量作随机访问造成,也有可能磁盘出现瓶颈(块操作)。

id

空闲时间百分比

实例2:vmstat –a 显示活跃和非活跃内存,显示增加了inact和active列

示例3: vmstat -s 查看内存使用的详细信息

[root@zhaokk opt]# vmstat -s
1882740 K total memory
440544 K used memory
400700 K active memory
198592 K inactive memory
1200708 K free memory
7252 K buffer memory
234236 K swap cache
0 K total swap
0 K used swap
0 K free swap
1445025 non-nice user cpu ticks
269 nice user cpu ticks
1271192 system cpu ticks
185869501 idle cpu ticks
32682 IO-wait cpu ticks
0 IRQ cpu ticks
548 softirq cpu ticks
0 stolen cpu ticks
2058857 pages paged in
10263968 pages paged out
0 pages swapped in
0 pages swapped out
1130998510 interrupts
1746041179 CPU context switches
1594017679 boot time
3647164 forks

示例4: vmstat -d 查看磁盘的读/写

[root@zhaokk opt]# vmstat -d
disk- ------------reads------------ ------------writes----------- -----IO------
       total merged sectors      ms  total merged sectors      ms    cur    sec
vda    54750   1689 4117714  628393 913459 621024 20528056 2208005      0    527

示例5: 查看/dev/sda1磁盘的读/写

[root@oracledb ~]# vmstat -p /dev/sda1 
sda1          reads   read sectors  writes    requested writes
666       5466          7         50
来源:https://www.cnblogs.com/xqzt/p/5448983.html

Lmbench 的安装

来源https://blog.csdn.net/askbai666888/article/details/7876087

原文:1.1.3 如何减少上下文切换

减少上下文切换的方法有无锁并发编程、CAS算法、使用最少线程和使用协程。

·无锁并发编程。多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一

些办法来避免使用锁,如将数据的ID按照Hash算法取模分段,不同的线程处理不同段的数据。

·CAS算法。Java的Atomic包使用CAS算法来更新数据,而不需要加锁。

·使用最少线程。避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这

样会造成大量线程都处于等待状态。

·协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换

自旋锁不适于单核cpu

自旋锁能用于中断上下文(中断屏蔽)?不能

锁总线,保证原子性

原文

1.1.4 减少上下文切换实战

这里没有实践,大概是dump线程信息,改变线程池任务量

第一步:用jstack命令dump线程信息,看看pid为3117的进程里的线程都在做什么。

sudo -u admin /opt/ifeve/java/bin/jstack 31177 > /home/tengfei.fangtf/dump17

第二步:统计所有线程分别处于什么状态,发现300多个线程处于WAITING(onobject-

monitor)状态。

[tengfei.fangtf@ifeve ~]$ grep java.lang.Thread.State dump17 | awk '{print $2$3$4$5}'
        | sort | uniq -c
 39 RUNNABLE
 21 TIMED_WAITING(onobjectmonitor)
 6 TIMED_WAITING(parking)
 51 TIMED_WAITING(sleeping)
 305 WAITING(onobjectmonitor)
 3 WAITING(parking)

第三步:打开dump文件查看处于WAITING(onobjectmonitor)的线程在做什么。发现这些线

程基本全是JBOSS的工作线程,在await。说明JBOSS线程池里线程接收到的任务太少,大量线程都闲着。

"http-0.0.0.0-7001-97" daemon prio=10 tid=0x000000004f6a8000 nid=0x555e in
    Object.wait() [0x0000000052423000]
 java.lang.Thread.State: WAITING (on object monitor)
 at java.lang.Object.wait(Native Method)
 - waiting on <0x00000007969b2280> (a org.apache.tomcat.util.net.AprEndpoint$Worker)
 at java.lang.Object.wait(Object.java:485)
 at org.apache.tomcat.util.net.AprEndpoint$Worker.await(AprEndpoint.java:1464)
 - locked <0x00000007969b2280> (a org.apache.tomcat.util.net.AprEndpoint$Worker)
 at org.apache.tomcat.util.net.AprEndpoint$Worker.run(AprEndpoint.java:1489)
 at java.lang.Thread.run(Thread.java:662)

第四步:减少JBOSS的工作线程数,找到JBOSS的线程池配置信息,将maxThreads降到100。

<maxThreads="250" maxHttpHeaderSize="8192"
 emptySessionPath="false" minSpareThreads="40" maxSpareThreads="75"
     maxPostSize="512000" protocol="HTTP/1.1"
 enableLookups="false" redirectPort="8443" acceptCount="200" bufferSize="16384"
 connectionTimeout="15000" disableUploadTimeout="false" useBodyEncodingForURI= "true">

第五步:重启JBOSS,再dump线程信息,然后统计WAITING(onobjectmonitor)的线程,发现减少了175个。WAITING的线程少了,系统上下文切换的次数就会少,因为每一次从

WAITTING到RUNNABLE都会进行一次上下文的切换。读者也可以使用vmstat命令测试一下。

[tengfei.fangtf@ifeve ~]$ grep java.lang.Thread.State dump17 | awk '{print $2$3$4$5}'
    | sort | uniq -c
   44 RUNNABLE
   22 TIMED_WAITING(onobjectmonitor)
   9 TIMED_WAITING(parking)
   36 TIMED_WAITING(sleeping)
   130 WAITING(onobjectmonitor)
1  WAITING(parking)

1.2 死锁

个人理解相当于GC算法的引用计数算法思想,互相引用,这里是互相持有,互相等待锁释放

原文demo

public class DeadLockDemo {

        private static String A = "A";
        private static String B = "B";
        public static void main(String[] args) {
            new DeadLockDemo().deadLock();
        }
        private void deadLock() {
            Thread t1 = new Thread(
            ()->{
                synchronized (A) {
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (B) {
                        System.out.println("1");
                    }
                }
            });
            Thread t2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (B) {
                        synchronized (A) {
                            System.out.println("2");
                        }
                    }
                }
            });
            t1.start();
            t2.start();
        }

}
原文是Thread.currentThread().sleep(2000);但是我这编译不通过

运行此段代码

使用jconsole监测死锁