文档章节

Java网络编程——3.线程

天子剑毅
 天子剑毅
发布于 2017/04/21 13:52
字数 3129
阅读 57
收藏 1

本来是想网上转载的,但看了半天写的都不是很好,看来还是要根据《Java网络编程》这本书自己码字了。在20世纪90年代初期,Internet只有几百万用户,大多数FTP服务器会为每个连接创建(fork)一个新的进程,由于进程是相当重量级的,太多进程会很快让服务器吃不消,早期Web服务器也有这个问题。当然进程可以重用,但还是使用轻量级的线程来处理连接来得好。用线程来代替进程,再结合可重用的线程池,服务器的运行可以快9倍多!但这种性能的提升不是没有代价的,它会增加程序的复杂性。下面我们就来学习下Java线程的相关知识

1、运行线程

线程如果以小写字母t打头(thread),表示虚拟机中的一个单独、独立的执行路径。以大写字母T打头,则是java.lang.Thread类的一个实例。继承这个类,覆盖其run()方法,或者实现Runnable接口,将Runnable对象传递给Thread构造函数。run()方法完成时,线程也就消失了,run()对于线程就像main()方法对于非线程化传统程序的作用一样。多线程程序会在所有非守护线程(nondaemon thread)都返回时才退出

2、从线程返回信息

我们不必在主程序中无限循环来重复地轮询线程是否结束,而是让线程告诉主程序它什么时候结束,通过回调(callback)来实现。Java5 引入了多线程编程的一个新方法,通过隐藏细节可以更容易地处理回调,创建一个ExecutorService,它会根据需要为你创建线程。这里就可以想ExecutorService提交Callable任务,对于每个Callable任务,会分别得到一个Future,之后可以向Future请求得到任务的结果。

如果结果已经准备就绪,就会立即得到这个结果,如果还没准备好,线程会阻塞,直到结果准备就绪。这种做法的好处是,你可以创建很多不同的线程,然后按你需要的顺序得到你需要的答案。

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class TestCallBack {

    public static int max(int[] data) throws InterruptedException, ExecutionException{
        if(data.length == 1){
            return data[0];
        } else if(data.length == 0){
            throw new IllegalArgumentException();
        }
        FindMaxTask task1 = new FindMaxTask(data,0,data.length/2);
        FindMaxTask task2 = new FindMaxTask(data,data.length/2,data.length);

        ExecutorService service = Executors.newFixedThreadPool(2);
        
        Future<Integer> future1 = service.submit(task1);
        Future<Integer> future2 = service.submit(task2);

        return Math.max(future1.get(),future2.get());
    }

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        int[] array = {1,2,4,6,8,9,0,12,3,45,67,89,88};
        System.out.println(max(array));
    }

}

class FindMaxTask implements Callable<Integer> {

    private int[] data;
    private int start;
    private int end;

    public FindMaxTask(int[] data, int start, int end) {
        this.data = data;
        this.start = start;
        this.end = end;
    }

    @Override
    public Integer call() throws Exception {
        int max = Integer.MIN_VALUE;
        for (int i = start; i < end; i++) {
            if (data[i] > max) {
                max = data[i];
            }
        }
        return max;
    }

}

Future是一种非常方便的做法,可以启动多个线程来处理一个问题的不同部分。这个例子只使用了两个线程,不过完全可以使用更多的线程,并重用这些线程来完成多个任务。

3、同步

多线程程序的缺点是,如果两个线程同时访问同一个资源,其中一个就必须等待另一个结束。如果其中一个没有等待,资源就可能被破坏。因此,只要有多个线程共享资源,都必须考虑同步。

synchronized修饰符,可以对代码块(指定对象)或整个方法体(当前对象,this引用)同步。仅仅像所有方法添加synchronized修饰符不是一劳永逸的解决方案。首先,它使得很多VM的性能严重下降。其次,它大大增加了死锁的可能性。第三,并不总是对象本身需要防止同时修改或访问,可能并不能保护真正需要保护的对象。

对于由线程调度引起的不一致的行为,同步并不总是这个问题最好的解决方案。尽可能的使用局部变量、线程安全的方法, 或将非线程安全的类用作为线程安全的类的一个私有字段。

有些情况下,你可以使用java.util.concurrent.atomic包中特意设计为保证线程安全但可变的类,如可以不使用int而使用AtomicInteger。不过需要说明,这不会让对象本身也是线程安全的,只是该引用变量的获取和设置是线程安全的,这些类可能比其相应基本类型的同步访问快得多。对于映射和列表等集合,可以使用java.util.Collections的方法把它们包装在一个线程安全的版本中,如集合foo,可以用Collections.synchronizedSet(foo)得到这个集合的一个线程安全视图。不管怎样,要认识到这只是单个的原子方法调用。如果需要作为一个原子连续地完成两个操作,中间不能有中断,就需要 同步。

4、死锁

同步会导致另一个可能的问题:死锁(deadlock)。如果两个线程需要独占访问同样的一个资源集,而每个线程分别有这些资源的不同子集的锁,就会发生死锁,两个线程都不愿意放弃已经拥有的资源,就会进入无限停止状态。

死锁是偶发性的bug,很难检测。1000次里有999次程序都能完全正常地运行,只有在极少情况下会毫无征兆地挂起。要防止死锁,最重要的技术是避免不必要的同步,同步应该是确保线程安全的最后一道防线。如果确实需要同步,要保持同步块尽可能小,而且尽量不要一次同步多个对象。但这可能很困难,因为你的代码会调用Java类库的很多方法,这些方法可能会在你不知情的情况下同步一些对象。因此,实际同步的对象可能比你预想的要多得多。

最好仔细考虑是否会出现死锁问题,并围绕这一点来设计你的代码。如果多个对象需要操作相同的共享资源集,要确保以相同的顺序请求这些资源,如果能做到这一点,死锁就不是问题。

5、线程调度

当多个线程可以同时运行时,必须考虑线程调度问题。每个线程都有一个优先级,指定为一个0到10的整数,10最高0最低,默认优先级为5。当多个线程可以运行时,虚拟机通常只运行最高优先级的线程。

每个虚拟机都有一个线程调度器,确定在给定时刻运行哪个线程。主要有两种线程调度:抢占式(preemptive)和协作式(cooperative)。抢占式线程调度器确定一个线程正常地轮到其CPU时间时,会暂停这个线程,将CPU控制权交给另外的线程。协作式线程调度器在将CPU控制权交给其他线程前,会等待正在运行的线程自己暂停。使用协作式线程调度器的虚拟器更容易使线程陷入“饥饿”(CPU资源被高优先级的非协作线程占用了),目前大多数虚拟机都使用抢占式线程调度。

为了能让其他线程有机会运行,一个线程有10种方式可以暂停或者指示它准备暂停,这包括:

  • 可以对I/O阻塞
  • 可以对同步对象阻塞
  • 可以放弃
  • 可以休眠
  • 可以连接另一个线程
  • 可以等待一个对象
  • 可以结束
  • 可以被更高优先级线程抢占
  • 可以被挂起
  • 可以停止

阻塞:任务时候线程必须停下来等待它没有的资源时,就会发生阻塞。最常见的方式是对I/O阻塞,网络程序经常会在等待数据从网络到达或向网络发送数据时阻塞,阻塞即使几毫秒也足够其他线程用来完成主要的任务。线程在进入一个同步方法或代码块时也会阻塞。这两种阻塞都不会释放线程已经拥有的锁。对于I/O阻塞有两种情况:最后I/O终将不再阻塞而线程将继续执行;或者将抛出IOException异常,然后线程退出这个同步块或方法并释放它的锁。如果一个线程等待第二个线程拥有的锁,而第二个线程等待第一个线程拥有的锁,就会导致死锁

放弃:要让线程放弃控制权,第二种方式是显式地放弃。线程可以通过调用Thread.yield()静态方法来做到,这将通知虚拟机,如果有另一个线程准备运行,可以运行该线程。放弃并不会释放这个线程拥有的锁,在理想情况下线程放弃时不应当作任何同步(防阻塞)。

休眠:休眠是更有力的放弃方式,放弃只是表示线程愿意暂停,让其他有相同优先级的线程有机会运行,而休眠不管有没有其他线程准备运行,休眠线程都会暂停。这样一来,不只是其他有相同优先级的线程得到机会,还会给较低优先级的线程一个运行的机会。休眠的线程仍然拥有它已经获得的所有锁,要避免在同步方法或块内让线程休眠(防阻塞)。通过调用Thread.sleep()方法,线程可以进入休眠。通过调用休眠线程的interrupt()方法唤醒。

连接线程:一个线程可能需要另一个线程的结果,Java提供了join()方法,允许一个线程在急需执行前等待另一个线程结束。Java5之后,原来需要join()的设计现在用Executor和Future可以更容易地实现。

等待一个对象:线程可以等待(wait)一个它锁定的对象,在等待时,它会释放这个对象的锁并暂停,直到它得到其他线程的通知(时间到期或线程被中断)。要等待某个特定的对象,希望暂停的线程首先必须使用synchronized获得这个对象的锁。wait()方法不在Thread类中,而是在java.lang.Object类中,因此可以在任何类的任何对象调用这些方法。时间到期(sleep()和join()的timeout一样)时,线程会从紧挨着wait()调用之后的语句继续执行,如果线程不能立即重新获得所等待的对象的锁,它可能仍要阻塞一段时间。在其他线程和这个线程所等待的对象上调用notify()或notifyAll()方法时,就会发生通知,来唤醒等待对象的线程。

结束:线程要以合理的方式放弃CPU控制权,最后一种方法是结束(finishing)。当run()方法返回时,线程将撤销,其他线程可以接管CPU。另一方面,如果run()方法太简单,总是很快就结束,而不会阻塞,那么就要考虑有没有必要生成一个线程,虚拟机在建立和撤销线程时会有很大的开销,如果线程会在极短的时间内结束,那么使用一次简单的方法调用而不是单独的线程可能结束得更快。

6、线程池和Executor

线程可以提升程序性能,尤其是I/O受限的程序,但是线程本身也有开销。启动线程及线程撤销后的清理,都需要虚拟机做大量工作,尤其是生成几百个线程的程序。即时线程能很快结束,也会加重垃圾回收器或VM其他部分的负担而影响性能,运行线程之前切换也会带来开销。最重要的是CPU提供的资源是有限的,过多的线程会将CPU和内存浪费在线程管理上。

利用java.util.concurrent中的Executors类,可以非常容易地建立线程池。

© 著作权归作者所有

共有 人打赏支持
天子剑毅
粉丝 2
博文 34
码字总数 40324
作品 0
杭州
架构师
私信 提问
基于JVM原理、JMM模型和CPU缓存模型深入理解Java并发编程

许多以Java多线程开发为主题的技术书籍,都会把对Java虚拟机和Java内存模型的讲解,作为讲授Java并发编程开发的主要内容,有的还深入到计算机系统的内存、CPU、缓存等予以说明。实际上,在实...

leoliu168
2018/11/08
0
0
.NET、C#和ASP.NET三者之间的区别

.NET、C#和ASP.NET三者之间的区别如下: 一、什么是.NET? .NET是微软公司下的一个开发平台,.NET核心就是.NET Framwork(.NET框架)是.NET程序开发和运行的环境,在这个平台下可以用不同的语...

曹庆军
2018/11/28
0
0
Java 使用 happen-before 规则实现共享变量的同步操作

前言 熟悉 Java 并发编程的都知道,JMM(Java 内存模型) 中的 happen-before(简称 hb)规则,该规则定义了 Java 多线程操作的有序性和可见性,防止了编译器重排序对程序结果的影响。按照官方的...

stateIs0
2018/01/20
0
0
Java架构师六大互联网公司面试经历总结

Java架构师面试经历 Java架构师面试经历从58同城——华为 ——招商银行网络中心——金蝶互联网公司GR——苏宁易购 ——蚂蚁金服,看完鬼知道我经历了什么,但是每一次都是成长。本人从2013年...

java知识分子
04/09
0
0
基于JVM原理JMM模型和CPU缓存模型深入理解Java并发编程

许多以Java多线程开发为主题的技术书籍,都会把对Java虚拟机和Java内存模型的讲解,作为讲授Java并发编程开发的主要内容,有的还深入到计算机系统的内存、CPU、缓存等予以说明。实际上,在实...

Java高级技术
2018/11/21
0
0

没有更多内容

加载失败,请刷新页面

加载更多

安装数据库 常见问题

数据库重置密码 如果MySQL数据库用户的密码设置过于简单,数据库在用户登录后会提示重置密码,并且不接受简单的密码。 提示需要重置密码: ERROR 1820 (HY000): You must reset your passwo...

狼王黄师傅
4分钟前
0
0
三种方式拿到反射的入口

public class a_1 { public static void main(String args[]) throws ClassNotFoundException { //三种方式拿到反射的入口 System.out.println(Class.forName("reflect.com.Son")); System.o......

南桥北木
16分钟前
0
0
Macbook 安装PhotoShop 总提示安装包损坏的问题

问题描述 今天在给Mac10.12安装Adobe Photoshop的时候一直提示Error The installation cannot continue as the installer file may be damaged. Download the installer file again.起初以为......

Carlyle_Lee
23分钟前
1
0
Java 帝国对 Python 的渗透能成功吗?哈哈

引子 Java 帝国已经成立20多年,经过历代国王的励精图治,可以说是地大物博,码农众多。 可是国王依然不满足,整天想着如何继续开拓疆土, 这一天晚上他又把几个重臣招来商议了。 IO大臣说:...

边鹏_尛爺鑫
今天
11
0
分布式事务解决方案框架(LCN)

什么是XA接口 XA是一个分布式事务协议,由Tuxedo提出。XA中大致分为两部分:事务管理器和本地资源管理器。其中本地资源管理器往往由数据库实现,比如Oracle、DB2这些商业数据库都实现了XA接口...

群星纪元
今天
8
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部