导图社区 Java多线程
用多线程只有一个目的,那就是更好的利用cpu的资源,因为所有的多线程代码都可以用单线程来实现。说这个话其实只有一半对,因为反应“多角色”的程序代码,最起码每个角色要给他一个线程吧,否则连实际场景都无法模拟,当然也没法说能用单线程来实现:比如最常见的“生产者,消费者模型”。
编辑于2021-06-03 13:21:52Java是一门面向对象的编程语言,不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承、指针等概念,因此Java语言具有功能强大和简单易用两个特征。Java语言作为静态面向对象编程语言的代表,极好地实现了面向对象理论,允许程序员以优雅的思维方式进行复杂的编程
用多线程只有一个目的,那就是更好的利用cpu的资源,因为所有的多线程代码都可以用单线程来实现。说这个话其实只有一半对,因为反应“多角色”的程序代码,最起码每个角色要给他一个线程吧,否则连实际场景都无法模拟,当然也没法说能用单线程来实现:比如最常见的“生产者,消费者模型”。
平和保存和搜索的一些好用的网站,分享一波,好用拿走。
社区模板帮助中心,点此进入>>
Java是一门面向对象的编程语言,不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承、指针等概念,因此Java语言具有功能强大和简单易用两个特征。Java语言作为静态面向对象编程语言的代表,极好地实现了面向对象理论,允许程序员以优雅的思维方式进行复杂的编程
用多线程只有一个目的,那就是更好的利用cpu的资源,因为所有的多线程代码都可以用单线程来实现。说这个话其实只有一半对,因为反应“多角色”的程序代码,最起码每个角色要给他一个线程吧,否则连实际场景都无法模拟,当然也没法说能用单线程来实现:比如最常见的“生产者,消费者模型”。
平和保存和搜索的一些好用的网站,分享一波,好用拿走。
Java多线程
1. 进程与线程
1.1. 进程
1.1.1. 进程是程序的一次执行,具有独立功能的程序及其数据在处理机上顺序执行的活动。
1.1.2. 进程是系统进行资源分配和调度的独立单位,每一个进程都有一个独立的内存空间和系统资源
1.1.3. 进程:程序在运行时会有一片空间,内存会分配一个地址,进程就是用于定义空间(标识空间),用于封装控制单元。
1.1.4. 一个进程崩溃后,在保护模式下不会对其它进程产生影响。
1.2. 线程
1.2.1. 线程是进程的一个执行路径,一个进程中至少有一个线程
1.2.2. 线程就是进程中的一个控制单元,控制着进程的执行
1.2.3. 线程是CPU分配资源的基本(最小)单位
1.2.4. 线程有自己的堆栈(程序计数器)和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉。
1.3. 进程与线程的区别
1.3.1. 一个进程中有多个线程,多个线程共享进程的堆和方法区资源,但是每个线程有自己的程序计数器和栈区域。
1.3.2. Java虚拟机启动会有一个进程java.exe,该进程中至少一个线程负责Java程序运行,这个线程的代码存在于main中。该线程称之为主线程。*次级线程--例如:垃圾回收
1.4. 多线程的意义
1.4.1. 开启线程的目的是为了运行指定的代码;
2. 相关知识
2.1. 程序计数器:是一块内存区域,用来记录线程当前要执行的指令地址 。
2.2. 堆:是一个进程中最大的一块内存,堆是被进程中的所有线程共享的。
2.3. 栈:用于存储该线程的局部变量,这些局部变量是该线程私有的,除此之外还用来存放线程的调用栈祯。
2.4. 方法区:则用来存放 JVM 加载的类、常量及静态变量等信息,也是线程共享的 。
3. 并发与并行
3.1. 并发intercurrent
3.1.1. 时间段
3.1.1.1. 是指同一个时间段内多个任务同时都在执行,并且都没有执行结束。
3.1.2. 并发任务强调在一个时间段内同时执行,多个线程在单个核心运行,同一时间点一个线程运行,系统不停切换线程,并发的多个任务在单位时间内不一定同时在执行 。
3.1.3. 并发常见问题
3.1.3.1. 共享内存不可见性问题
3.1.4. 比喻
3.1.4.1. 你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭
3.1.5. 并发里面同时发生两个事情,是并行还是串行,看CUP,单核就是串行,多核就是并行
3.2. 并行
3.2.1. 并行是在不同实体上的多个事件。
3.2.2. 时间点
3.2.2.1. 是说在单位时间内多个任务同时在执行 。
3.2.3. 每个线程分配独立的核心,线程同时运行
3.2.4. 一边一边
3.2.4.1. 你吃饭吃到一半,电话来了,你一边打电话一边吃饭
3.3. 串行
3.3.1. 在多线程程序中,如果在一个线程运行的过程中要用到另一个线程的运行结果,则可以进行线程的串行化处理。
3.3.2. 多个任务,执行完再执行另一个
3.3.3. 比喻
3.3.3.1. 你吃饭吃到一半,电话来了,你一直到吃完了以后才去接
3.4. 在多线程编程实践中,线程的个数往往多于CPU的个数,所以一般都称多线程并发编程而不是多线程并行编程。
4. 线程安全
4.1. 线程安全问题
为什么会出现线程安全问题? 计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中会涉及到数据的读取和写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。 为了处理这个问题,在CPU里面就有了高速缓存(Cache)的概念。当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。 CPU处理: 主存-->高速缓存--CPU处理数据--高速缓存-->主存 线程操作的数据在高速缓存中,那么多个线程从主存复制的共享数据相同,处理的结果不同,数据返回主存时会出现安全问题。
4.2. synchronized 的内存语义:
synchronized(同步):这个内存语义就可以解决共享变量内存可见性问题。 进入synchronized块的内存语义是把在synchronized块内使用到的变量从线程的工作内存中清除,这样在synchronized块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。 退出synchronized块的内存语义是把在synchronized块内对共享变量的修改刷新到主内存。会造成上下文切换的开销,独占锁,降低并发性。
4.3. Volatile的理解
volatile():该关键字可以确保对一个变量的更新对其他线程马上可见。当一个变量被声明为volatile时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。当其他线程读取该共享变量时-,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。 volatile的内存语义和synchronized有相似之处,具体来说就是,当线程写入了volatile变量值时就等价于线程退出synchronized同步块(把写入工作内存的变量值同步到主内存),读取volatile变量值时就相当于进入同步块(先清空本地内存变量值,再从主内存获取最新值)。不能保证原子性。
5. 经典问题
pubic class SellStation implement Runnable{ private Object obj = new Object(); static int ticket=100; public void run()(//重写run方法,实现卖票机制 while(ticket>1){ synchronized(obj){//加锁 try{ Thread.sleep(200); }catch(Exception e){ e.printStackTrace(); } //获取当前正在进行的线程名(正在买票的窗口) System.out.println( Thread.currentThread(). getName()+"卖出了第"+ticket+"张票"); ticket--; } } } } public static void main(string[] args) throws Exception { //实例化卖票站台 Sellstation t = new Sellstation(); //创建3个窗口来卖票,参数t代表对同一张票的处理方式 Thread t1= new Thread(t,"窗口1"); Thread t2= new Thread(t,"窗口2"); Thread t3= new Thread(t,"窗口3"); //将3个线程启动 t1.start(); t2.start(); t3.start(); }
5.1. 出现的问题
通过分析,发现,打印出0,-1,-2等错票。多线程的运行出现了安全问题。 *多线程小心安全问题 问题的原因: 当多条语句在操纵同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没执行完,另一个线程参与进来执行,导致共享数据的错误。 解决办法: 对多条操作共享数据的语句,只能让一个线程都执行完。在执行过程中,其他线程不可以参与执行。 Java对于多线程的安全问题提供了专业的解决方式。就是同步代码块。 synchronized(对象){ 需要被同步的代码 } 同步锁,线程进入同步代码块,先改变标志位,防止其他进程运行,执行同步代码块内内容,然后改变标志位 (共享池)锁,监视器 对象如同锁。持有锁的线程可以在同步中执行。没有持有锁的线程即使获取cpu的执行权,也进不去,因为没有获取锁。 同步的前提: 1.必须要有两个或者两个以上的线程。 2.必须是多个线程使用同一个锁。 3.必须保证同步中只能有一个线程在运行。 好处:解决了多线程的安全问题。 弊端:多个线程需要判断锁,较为消耗资源. 如何找问题: 1,明确哪些代码是多线程运行代码。 2,明确共享数据。 3,明确多线程运行代码中哪些语句是操作共享数据的。 同步有两种表示形式: 1.同步代码块 2.同步关键字作为修饰符修饰函数,使其具有同步特性 注意哪些语句需要同步 同步函数用的是哪一个锁呢? 函数需要被对象调用。那么函数都有一个所属对象引用。就是this。所以同步函数使用的锁是this。 如果同步函数被静态修饰后,使用的锁是什么呢? 通过验证,发现不在是this。因为静态方法中也不可以定义this。 静态进内存是,内存中没有本类对象,但是一定有该类对应的字节码文件对象。 类名.class该对象的类型是Class 同步的对象是类名.class,静态的同步方法,使用的锁是该方法所在类的字节码文件对象,类名.class
6. 死锁
6.1. 锁的特性
6.1.1. 互斥(mutual exclusion)
6.1.1.1. 互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,
6.1.1.2. 这样,一次就只有一个线程能够使用该共享数据
6.1.2. 可见性(visibility)
6.1.2.1. 必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的
6.2. 锁的类型
6.2.1. 可重入锁
6.2.1.1. 可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁
6.2.2. 公平锁
6.2.2.1. 公平锁是指多个线程按照申请锁的顺序来获取锁
6.2.3. 非公平锁
6.2.3.1. 非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁
6.3. Java多线程加锁机制
6.3.1. ·Synchronized
6.3.2. ·显式Lock
6.4. 死锁的四个必要条件
6.4.1. 互斥条件
6.4.1.1. 资源不能被共享,只能被同一个进程使用
6.4.2. 请求与保持条件
6.4.2.1. 已经得到资源的进程可以申请新的资源
6.4.3. 非剥夺条件
6.4.3.1. 已经分配的资源不能从相应的进程中获取,被强制剥夺
6.4.4. 循环等待条件
6.4.4.1. 系统中若干进程组成环路,该环路中每个进程都在等待相邻进程占用的资源
6.5. 处理死锁的方式
6.5.1. - 忽略该问题,也即鸵鸟算法。当发生了什么问题时,不管他,直接跳过,无视它;
6.5.2. - 检测死锁并恢复;
6.5.3. - 资源进行动态分配;
6.5.4. - 破除上面的四种死锁条件之一。
7. 线程池
7.1. 概念
7.1.1. 线程池:本质上是一种对象池,用于管理线程资源,在任务执行前,需要从线程池中拿出-线程来执行,在任务执行完成之后,需要把线程放回线程池。
7.2. 使用线程池的目的
7.2.1. · 重用线程池的线程,避免因为线程的创建和销毁锁带来的性能开销
7.2.2. · 降低资源的消耗。线程本身是一种资源,创建和销毁线程会有CPU开销;创建的线程也会占用一定的内存使用线程池可以进行统一的分配,调优和监控。
7.2.3. · 提高任务执行的响应速度。任务执行时,可以不必等到线程创建完之后再执行
7.3. 使用Executors工厂类产生线程池
7.4. 使用Java8增强的ForkJoinPool产生线程池
8. 线程通信
8.1. 借助于Object类的wait()、notify()和notifyAll()实现通信
借助于Object类的wait()、notify()和notifyAll()实现通信: 线程执行wait()后,就放弃了运行资格,处于冻结状态; 线程运行时,内存中会建立一个线程池,冻结状态的线程都存在于线程池中,notify()执行时唤醒的也是线程池中的线程,线程池中有多个线程时唤醒第一个被冻结的线程。 notifyall(), 唤醒线程池中所有线程。 注: (1) wait(), notify(),notifyall()都用在同步里面,因为这3个函数是对持有锁的线程进行操作,而只有同步才有锁,所以要使用在同步中; (2) wait(),notify(),notifyall(), 在使用时必须标识它们所操作的线程持有的锁,因为等待和唤醒必须是同一锁下的线程;而锁可以是任意对象,所以这3个方法都是Object类中的方法。 JDK1.5中提供了多线程升级解决方案。 将同步synchronized替换成现实Lock操作。 将object中的wait,notify notifyall,替换了condition对象。
8.1.1. 单个消费者例子
单个消费者生产者例子如下: class Resource{ //生产者和消费者都要操作的资源 private String name; private int count=1; private boolean flag=false; public synchronized void set(String name){ if(flag) try{wait();}catch(Exception e){} this.name=name+"---"+count++; System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name); flag=true; this.notify(); } public synchronized void out(){ if(!flag) try{wait();}catch(Exception e){} System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name); flag=false; this.notify(); } } class Producer implements Runnable{ private Resource res; Producer(Resource res){ this.res=res; } public void run(){ while(true){ res.set("商品"); } } } class Consumer implements Runnable{ private Resource res; Consumer(Resource res){ this.res=res; } public void run(){ while(true){ res.out(); } } } public class ProducerConsumerDemo{ public static void main(String[] args){ Resource r=new Resource(); Producer pro=new Producer(r); Consumer con=new Consumer(r); Thread t1=new Thread(pro); Thread t2=new Thread(con); t1.start(); t2.start(); } }//运行结果正常,生产者生产一个商品,紧接着消费者消费一个商品。 -------------------------------------------- 但是如果有多个生产者和多个消费者,上面的代码是有问题,比如2个生产者,2个消费者,运行结果就可能出现生产的1个商品生产了一次而被消费了2次,或者连续生产2个商品而只有1个被消费,这是因为此时共有4个线程在操作Resource对象r, 而notify()唤醒的是线程池中第1个wait()的线程,所以生产者执行notify()时,唤醒的线程有可能是另1个生产者线程,这个生产者线程从wait()中醒来后不会再判断flag,而是直接向下运行打印出一个新的商品,这样就出现了连续生产2个商品。 为了避免这种情况,修改代码如下: -------------------------------------------- class Resource{ private String name; private int count=1; private boolean flag=false; public synchronized void set(String name){ while(flag) /*原先是if,现在改成while,这样生产者线程从冻结状态醒来时,还会再判断flag.*/ try{wait();}catch(Exception e){} this.name=name+"---"+count++; System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name); flag=true; this.notifyAll();/*原先是notity(), 现在改成notifyAll(),这样生产者线程生产完一个商品后可以将等待中的消费者线程唤醒,否则只将上面改成while后,可能出现所有生产者和消费者都在wait()的情况。*/ } public synchronized void out(){ while(!flag) /*原先是if,现在改成while,这样消费者线程从冻结状态醒来时,还会再判断flag.*/ try{wait();}catch(Exception e){} System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name); flag=false; this.notifyAll(); /*原先是notity(), 现在改成notifyAll(),这样消费者线程消费完一个商品后可以将等待中的生产者线程唤醒,否则只将上面改成while后,可能出现所有生产者和消费者都在wait()的情况。*/ } } public class ProducerConsumerDemo{ public static void main(String[] args){ Resource r=new Resource(); Producer pro=new Producer(r); Consumer con=new Consumer(r); Thread t1=new Thread(pro); Thread t2=new Thread(con); Thread t3=new Thread(pro); Thread t4=new Thread(con); t1.start(); t2.start(); t3.start(); t4.start(); } }
8.2. 使用Condition控制线程通信
使用Condition控制线程通信: jdk1.5中,提供了多线程的升级解决方案为: (1)将同步synchronized替换为显式的Lock操作; (2)将Object类中的wait(), notify(),notifyAll()替换成了Condition对象,该对象可以通过Lock锁对象获取; (3)一个Lock对象上可以绑定多个Condition对象,这样实现了本方线程只唤醒对方线程,而jdk1.5之前,一个同步只能有一个锁,不同的同步只能用锁来区分,且锁嵌套时容易死锁。 -------------------------------------------- class Resource{ private String name; private int count=1; private boolean flag=false; private Lock lock = new ReentrantLock();/*Lock是一个接口,ReentrantLock是该接口的一个直接子类。*/ private Condition condition_pro=lock.newCondition(); /*创建代表生产者方面的Condition对象*/ private Condition condition_con=lock.newCondition(); /*使用同一个锁,创建代表消费者方面的Condition对象*/ public void set(String name){ lock.lock();//锁住此语句与lock.unlock()之间的代码 try{ while(flag) condition_pro.await(); //生产者线程在conndition_pro对象上等待 this.name=name+"---"+count++; System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name); flag=true; condition_con.signalAll(); } finally{ lock.unlock(); //unlock()要放在finally块中。 } } public void out(){ lock.lock(); //锁住此语句与lock.unlock()之间的代码 try{ while(!flag) condition_con.await(); //消费者线程在conndition_con对象上等待 System.out.println(Thread.currentThread().getName()+"...消费者..."+this.name); flag=false; condition_pro.signqlAll(); /*唤醒所有在condition_pro对象下等待的线程,也就是唤醒所有生产者线程*/ } finally{ lock.unlock(); } } }
8.3. 使用阻塞队列(BlockingQueue)控制线程通信
BlockingQueue是一个接口,也是Queue的子接口。**BlockingQueue具有一个特征:**当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则线程被阻塞;但消费者线程试图从BlockingQueue中取出元素时,如果队列已空,则该线程阻塞。程序的两个线程通过交替向BlockingQueue中放入元素、取出元素,即可很好地控制线程的通信。 **BlockingQueue提供如下两个支持阻塞的方法:** **(1)put(E e):**尝试把Eu元素放如BlockingQueue中,如果该队列的元素已满,则阻塞该线程。 **(2)take():**尝试从BlockingQueue的头部取出元素,如果该队列的元素已空,则阻塞该线程。 **BlockingQueue继承了Queue接口,当然也可以使用Queue接口中的方法,这些方法归纳起来可以分为如下三组:** **(1)**在队列尾部插入元素,包括add(E e)、offer(E e)、put(E e)方法,当该队列已满时,这三个方法分别会抛出异常、返回false、阻塞队列。 **(2)**在队列头部删除并返回删除的元素。包括remove()、poll()、和take()方法,当该队列已空时,这三个方法分别会抛出异常、返回false、阻塞队列。 **(3)**在队列头部取出但不删除元素。包括element()和peek()方法,当队列已空时,这两个方法分别抛出异常、返回false。 ---------------------------------------------- BlockingQueue接口包含如下5个实现类: ArrayBlockingQueue :基于数组实现的BlockingQueue队列。 LinkedBlockingQueue:基于链表实现的BlockingQueue队列。 PriorityBlockingQueue:它并不是保准的阻塞队列,该队列调用remove()、poll()、take()等方法提取出元素时,并不是取出队列中存在时间最长的元素,而是队列中最小的元素。 它判断元素的大小即可根据元素(实现Comparable接口)的本身大小来自然排序,也可使用Comparator进行定制排序。 SynchronousQueue:同步队列。对该队列的存、取操作必须交替进行。 DelayQueue:它是一个特殊的BlockingQueue,底层基于PriorityBlockingQueue实现,不过,DelayQueue要求集合元素都实现Delay接口(该接口里只有一个long getDelay()方法), DelayQueue根据集合元素的getDalay()方法的返回值进行排序。
8.3.1. 示例
import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; public class BlockingQueueTest{ public static void main(String[] args)throws Exception{ //创建一个容量为1的BlockingQueue BlockingQueue b=new ArrayBlockingQueue(1); //启动3个生产者线程 new Producer(b).start(); new Producer(b).start(); new Producer(b).start(); //启动一个消费者线程 new Consumer(b).start(); } } class Producer extends Thread{ private BlockingQueue b; public Producer(BlockingQueue b){ this.b=b; } public synchronized void run(){ String [] str=new String[]{ "java", "struts", "Spring" }; for(int i=0;i System.out.println(getName()+"生产者准备生产集合元素!"); try{ b.put(str[i%3]); sleep(1000); //尝试放入元素,如果队列已满,则线程被阻塞 }catch(Exception e){System.out.println(e);} System.out.println(getName()+"生产完成:"+b); } } } class Consumer extends Thread{ private BlockingQueue b; public Consumer(BlockingQueue b){ this.b=b; } public synchronized void run(){ while(true){ System.out.println(getName()+"消费者准备消费集合元素!"); try{ sleep(1000); //尝试取出元素,如果队列已空,则线程被阻塞 b.take(); }catch(Exception e){System.out.println(e);} System.out.println(getName()+"消费完:"+b); } } }
9. 线程同步
java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查),将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用,从而保证了该变量的唯一性和准确性。 Java同步机制有4种实现方式:(部分引用网上资源) ① ThreadLocal ② synchronized( ) ③ wait() 与 notify() ④ volatile 目的:都是为了解决多线程中的对同一变量的访问冲突
9.1. 同步和异步
同步:A去烧水,B等着,水开了,B泡面。 异步:A去烧水,同时B放调料包,水开了,B泡面。 一、关键字: thread(线程)、thread-safe(线程安全)、并发(intercurrent) synchronized(同步的)、asynchronized(异步的)、 volatile(易变的)、atomic(原子的)、share(共享)
9.2. ThreadLocal
ThreadLocal ThreadLocal 保证不同线程拥有不同实例,相同线程一定拥有相同的实例,即为每一个使用该 变量的线程提供一个该变量值的副本,每一个线程都可以独立改变自己的副本,而不是与其它线程 的副本冲突。 优势:提供了线程安全的共享对象 与其它同步机制的区别:同步机制是为了同步多个线程对相同资源的并发访问,是为了多个线程之 间进行通信;而 ThreadLocal 是隔离多个线程的数据共享,从根本上就不在多个线程之间共享资源 ,这样当然不需要多个线程进行同步了。
9.3. synchronized
9.3.1. 同步方法
方法同步: 即有synchronized关键字修饰的方法。由于java的每个对象都有一个内置锁,当用此关键字修饰方法时,内置锁会保护整个方法。在调用该方法前,需要获得内置锁,否则就处于阻塞状态。 public synchronized void save(){} 注:synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,将会锁住整个类。
9.3.2. 同步代码块
即有synchronized关键字修饰的语句块。被该关键字修饰的语句块会自动被加上内置锁,从而实现同步。 ------------------------------------------- public class Bank { private int count =0;//账户余额 //存钱 public void addMoney(int money){ synchronized (this) { count +=money; } System.out.println(System.currentTimeMillis()+"存进:"+money); } //取钱 public void subMoney(int money){ synchronized (this) { if(count-money System.out.println("余额不足"); return; } count -=money; } System.out.println(+System.currentTimeMillis()+"取出:"+money); } //查询 public void lookMoney(){ System.out.println("账户余额:"+count); } } -------------------------------------------- 注:同步是一种高开销的操作,因此应该尽量减少同步的内容。通常没有必要同步整个方法,使用synchronized代码块同步关键代码即可。
9.4. wait()和notify()
sleep() vs wait() sleep是线程类(Thread)的方法,导致此线程暂停执行指定时间,把执行机会给其他线程,但是监 控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。 wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁 定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁 进入运行状态。 (如果变量被声明为volatile,在每次访问时都会和主存一致;如果变量在同步方法或者同步块中 被访问,当在方法或者块的入口处获得锁以及方法或者块退出时释放锁时变量被同步。)
9.5. volatile
使用特殊域变量(volatile)实现线程同步: • volatile关键字为域变量的访问提供了一种免锁机制; • 使用volatile修饰域相当于告诉虚拟机该域可能会被其他线程更新; • 因此每次使用该域就要重新计算,而不是使用寄存器中的值; • volatile不会提供任何原子操作,它也不能用来修饰final类型的变量。 ---------------------------------------------- public class SynchronizedThread { class Bank { private volatile int account = 100; public int getAccount() { return account; } /** * 用同步方法实现 * * @param money */ public synchronized void save(int money) { account += money; } /** * 用同步代码块实现 * * @param money */ public void save1(int money) { synchronized (this) { account += money; } } } class NewThread implements Runnable { private Bank bank; public NewThread(Bank bank) { this.bank = bank; } @Override public void run() { for (int i = 0; i // bank.save1(10); bank.save(10); System.out.println(i + "账户余额为:" +bank.getAccount()); } } } /** * 建立线程,调用内部类 */ public void useThread() { Bank bank = new Bank(); NewThread new_thread = new NewThread(bank); System.out.println("线程1"); Thread thread1 = new Thread(new_thread); thread1.start(); System.out.println("线程2"); Thread thread2 = new Thread(new_thread); thread2.start(); } public static void main(String[] args) { SynchronizedThread st = new SynchronizedThread(); st.useThread(); } --------------------------------------------- 注:多线程中的非同步问题主要出现在对域的读写上,如果让域自身避免这个问题,则就不需要修改操作该域的方法。用final域,有锁保护的域和volatile域可以避免非同步的问题。
9.5.1. volatile
volatile volatile 修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。 而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。 优势:这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。 缘由:Java 语言规范中指出,为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而 且只当线程进入或者离开同步代码块时才与共享成员变量的原始值对比。这样当多个线程同时与某 个对象交互时,就必须要注意到要让线程及时的得到共享成员变量的变化。而 volatile 关键字就 是提示 VM :对于这个成员变量不能保存它的私有拷贝,而应直接与共享成员变量交互。 使用技巧:在两个或者更多的线程访问的成员变量上使用 volatile 。当要访问的变量已在 synchronized 代码块中,或者为常量时,不必使用。 线程为了提高效率,将某成员变量(如A)拷贝了一份(如B),线程中对A的访问其实访问的 是B。只在某些动作时才进行A和B的同步,因此存在A和B不一致的情况。volatile就是用来避免这种 情况的。 volatile告诉jvm,它所修饰的变量不保留拷贝,直接访问主内存中的(读操作多时使用 较好;线程间需要通信,本条做不到) Volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性。这就是说线程能够自 动发现 volatile 变量的最新值。Volatile 变量可用于提供线程安全,但是只能应用于非常有限的 一组用例:多个变量之间或者某个变量的当前值与修改后值之间没有约束。 您只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理 想的线程安全,必须同时满足下面两个条件: 对变量的写操作不依赖于当前值;该变量没有包含在具有其他变量的不变式中。
9.6. 重入锁(Lock)
使用重入锁(Lock)实现线程同步: 在JavaSE5.0中新增了一个java.util.concurrent包来支持同步。ReentrantLock类是可重入、互斥、实现了Lock接口的锁,它与使用synchronized方法和快具有相同的基本行为和语义,并且扩展了其能力。ReenreantLock类的常用方法有: 1.ReentrantLock() : 创建一个ReentrantLock实例 2.lock() : 获得锁 3.unlock() : 释放锁 注:ReentrantLock()还有一个可以创建公平锁的构造方法,但由于能大幅度降低程序运行效率,不推荐使用 //只给出要修改的代码,其余代码同volatile ---------------------------------------------- class Bank { private int account = 100; //需要声明这个锁 private Lock lock = new ReentrantLock(); public int getAccount() { return account; } //这里不再需要synchronized public void save(int money) { lock.lock(); try{ account += money; }finally{ lock.unlock(); } } }
10. 创建线程
10.1. 多线程的优点
多线程的优点: (1)进程之间不能共享数据,线程可以; (2)系统创建进程需要为该进程重新分配系统资源,故创建线程代价比较小; (3)Java语言内置了多线程功能支持,简化了java多线程编程。 线程的状态: 被创建-start()--》运行 运行1-->sleep(time)-->冻结(放弃了执行资格)sleep时间到-->运行 运行2-->wait()-->冻结(不会自动返回) 冻结--》notify();唤醒 运行3--》消亡 stop();run()结束; 临时(阻塞)状态- 具备运行资格,但没有执行权。 原来线程都有自己默认的名称。 Thread-编号该编号从0开始。 getName() private String name; static Thread currentThread():获取当前线程对象, getName():获取线程名称。 设置线程名称:setName或者构造函数。
10.1.1. 多线程的弊端
多线程的弊端: 线程切换问题 线程安全问题 多个线程同时抢占一个资源(上锁) **在单核cpu上运行多线程没有意义, --线程同步、异步的区别
10.2. 创建线程和启动
10.2.1. 继承Thread类创建线程
(1)继承Thread类创建线程类 1. 定义一个继承Thread类的子类,并重写该类的run()方法; 2.创建Thread子类的实例,即创建了线程对象; 3.调用该线程对象的start()方法启动线程。 --------------------------------------------- class MyThread extends Thread { //步骤1:继承Thread类 @Override public void run() { //do something here } psvm(String[] args){ SomeThread oneThread = new SomeThread(); //2.创建线程实例 //步骤3:启动线程: oneThread.start(); } } 任何一个类只要继承Thread类就可以成为一个线程的主类。主方法就是run()方法。 -------------------------------------------- 例2: class MyThread extends Thread{ //线程的主体类 private String name;//线程对象名称 public MyThread(String name){ this.name=name; }//通过构造方法设置线程名称 @Override public void run(){//线程主方法 for(int x=0;x sop(this.name+"x="+x); } } } 启动多线程: public class TestDemo{ psvm(String[] args){ MyThread mt1= new MyThread("线程A");//创建线程对象 MyThread mt2= new MyThread("线程B");//创建线程对象 mt1.start();//启动线程 mt1.start();//启动线程 } }
10.2.2. 实现Runnable接口创建线程
(2)实现Runnable接口创建线程类 1. 定义Runnable接口的实现类,并重写该接口的run()方法; 2. 创建Runnable实现类的实例,并以此实例作为Thread的target(目标)对象,即该Thread对象才是真正的线程对象。 --------------------------------------------- class SomeRunnable implements Runnable { public void run() { //do something here } } class TestDemo{ Runnable oneRunnable = new SomeRunnable(); Thread oneThread = new Thread(oneRunnable); oneThread.start(); } --依旧是Thread对象.start(); 步骤: 1,定义类实现Runnable接口 2,覆盖Runnable接口中的run方法。 将线程要运行的代码存放在run方法中 3,通过Thread类建立线程对象。 4,将Runnab1e接口的子类对象作为实际参数传递给Thread类的构造函数。 为什么要将Runnable接口的子类对象传递给Thread的构造函数。因为,自定义的run方法所属的对象是Runnable接口的子类对象。 所以要让线程去指定指定对象的run方法。 就必须明确该run方法所属对象。 5,调用Thread类的start方法开启线程并调用Runnab1e接口子类的run方法。 实现方式和继承方式有什么区别呢? 1.继承与Thread类,实现与Rinable类 2.当该类无其他父类时,使用继承 当该类有一个父类时,不能继续继承Thread类,只能使用接口来实现多线程执行 3.实现的好处:避免了单继承的局限性。 在定义线程时,建立使用实现方式。 4.继承Thread:线程代码存放Thread子类run方法中。实现Runnable,线程代码存在接口的子类的run方法。 *线程的实现更加常用 --------------------------------------------- 示例2: class MyThread implements Runable{ //线程的主体类 private String name;//线程对象名称 public MyThread(String name){ this.name=name; }//通过构造方法设置线程名称 @Override public void run(){//线程主方法 for(int x=0;x sop(this.name+"x="+x); } } } 启动多线程: public class TestDemo{ psvm(String[] args){ MyThread mt1= new MyThread("线程A");//创建线程实现类对象 Thread T1= new Thread(mt1);//创建线程对象 Thread T2= new Thread(mt1);//创建线程对象 T1.start();//启动线程 T2.start();//启动线程 } }
区别
Thread和Runable的区别: 多线程的两种实现方式都需要一个线程的主类,而这个类可以实现 Runnable接口或继承 Thread类。不管使用何种方式都必须在子类中覆写run()方法,此方法为线程的主方法。 Thread类是 Runnable接口的子类,使用 Runnable接口可以避免单继承局限,以更加方便地实现数据共享的概念。
10.2.3. 通过Callable和Future创建线程
(3)通过Callable和Future创建线程 • 创建Callable接口的实现类,并实现call()方法,该call()方法将作为线程执行体,并且有返回值。 • 创建Callable实现类的实例,使用FutureTask类来包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值。 • 使用FutureTask对象作为Thread对象的target创建并启动新线程。 • 调用FutureTask对象的get()方法来获得子线程执行结束后的返回值,其中,Callable接口(也只有一个方法)定义如下: -------------------------------------------- public interface Callable { V call() throws Exception; } 步骤1:创建实现Callable接口的类SomeCallable(略); 步骤2:创建一个类对象: Callable oneCallable = new SomeCallable(); 步骤3:由Callable创建一个FutureTask对象: FutureTask oneTask = new FutureTask(oneCallable); 注释: FutureTask是一个包装器,它通过接受Callable来创建,它同时实现了 Future和Runnable接口。 步骤4:由FutureTask创建一个Thread对象: Thread oneThread = new Thread(oneTask); 步骤5:启动线程: oneThread.start();
10.2.4. run()和start()方法区别
run()和start()方法区别: run():仅仅是封装被线程执行的代码,直接调用是普通方法 start():首先启动了线程,然后再由jvm去调用该线程的run()方法。
10.3. 线程的生命周期(5种状态)
10.3.1. 新建状态
线程新增状态: 用new关键字和Thread类或其子类建立一个线程对象后,该线程对象就处于新生状态。-- 处于新生状态的线程有自己的内存空间,通过调用start()方法线程进入就绪状态(runnable). 注意:不能对已经启动的线程再次调用start()方法,否则会出现Java.lang.IllegalThreadStateException异常。
10.3.2. 就绪状态
线程就绪状态: 处于就绪状态的线程已经具备了运行条件,但还没有分配到CPU,处于线程就绪队列(尽管是采用队列形式,事实上,把它称为可运行池而不是可运行队列。因为cpu的调度不一定是按照先进先出的顺序来调度的),等待系统为其分配CPU。 等待状态并不是执行状态,当系统选定一个等待执行的Thread对象后,它就会从等待执行状态进入执行状态,系统挑选的动作称之为“cpu调度”。一旦获得CPU,线程就进入运行状态并自动调用自己的run()方法。 提示:如果希望子线程调用start()方法后立即执行,可以使用Thread.sleep()方式使主线程睡眠一伙儿,转去执行子线程。 就绪状态: 就绪等待(CPU未分配资源) -执行状态(调用run())
10.3.3. 运行状态
处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。 就绪-->运行: 处于就绪状态的线程,如果获得了cpu的调度,就会从就绪状态变为运行状态,执行run()方法中的任务。 运行-->就绪: 如果该线程失去了cpu资源,就会又从运行状态变为就绪状态。重新等待系统分配资源。也可以对在运行状态的线程调用yield()方法,它就会让出cpu资源,再次变为就绪状态。 运行-->阻塞: 注: 当发生如下情况是,线程会从运行状态变为阻塞状态: ①、线程调用sleep方法主动放弃所占用的系统资源 ②、线程调用一个阻塞式IO方法,在该方法返回之前,该线程被阻塞 ③、线程试图获得一个同步监视器,但更改同步监视器正被其他线程所持有 ④、线程在等待某个通知(notify) ⑤、程序调用了线程的suspend方法将线程挂起。不过该方法容易导致死锁,所以程序应该尽量避免使用该方法。 运行-->死亡: 当线程的run()方法执行完,或者被强制性地终止,例如出现异常,或者调用了stop()、desyory()方法等等,就会从运行状态转变为死亡状态。
10.3.4. 阻塞状态
处于运行状态的线程在某些情况下,如执行了sleep(睡眠)方法,或等待I/O设备等资源,将让出CPU并暂时停止自己的运行,进入阻塞状态。 在阻塞状态的线程不能进入就绪队列。只有当引起阻塞的原因消除时,如睡眠时间已到,或等待的I/O设备空闲下来,线程便转入就绪状态,重新到就绪队列中排队等待,被系统选中后从原来停止的位置开始继续运行。有三种方法可以暂停Threads执行: 在可执行性状态下,调用sleep(睡眠)、suspend(暂停)、wait(等待)等方法,线程阻塞。
10.3.4.1. 线程睡眠——sleep
线程睡眠sleep(): 如果我们需要让当前正在执行的线程暂停一段时间,并进入阻塞状态,则可以通过调用Thread的sleep()方法。 (1)sleep是静态方法,最好不要用Thread的实例对象调用它,因为它睡眠的始终是当前正在运行的线程,而不是调用它的线程对象,它只对正在运行状态的线程对象有效。如下面的例子: ------------------------------------- public class Test1 { public static void main(String[] args) throws InterruptedException { System.out.println(Thread.currentThread().getName()); MyThread myThread=new MyThread(); myThread.start(); myThread.sleep(1000);//这里sleep的就是main线程,而非myThread线程 Thread.sleep(10); for(int i=0;i System.out.println("main"+i); } } } ---------------------------------------------- (2)Java线程调度是Java多线程的核心,只有良好的调度,才能充分发挥系统的性能,提高程序的执行效率。但是不管程序员怎么编写调度,只能最大限度的影响线程执行的次序,而不能做到精准控制。因为使用sleep方法之后,线程是进入阻塞状态的,只有当睡眠的时间结束,才会重新进入到就绪状态,而就绪状态进入到运行状态,是由系统控制的,我们不可能精准的去干涉它,所以如果调用Thread.sleep(1000)使得线程睡眠1秒,可能结果会大于1秒。
10.3.4.2. 线程让步——yield
线程让步-yield: yield()方法和sleep()方法有点相似,它也是Thread类提供的一个静态的方法,它也可以让当前正在执行的线程暂停,让出cpu资源给其他的线程。但是和sleep()方法不同的是,它不会进入到阻塞状态,而是进入到就绪状态。yield()方法只是让当前线程暂停一下,重新进入就绪的线程池中,让系统的线程调度器重新调度器重新调度一次,完全可能出现这样的情况:当某个线程调用yield()方法之后,线程调度器又将其调度出来重新进入到运行状态执行。 实际上,当某个线程调用了yield()方法暂停之后,优先级与当前线程相同,或者优先级比当前线程更高的就绪状态的线程更有可能获得执行的机会,当然,只是有可能,因为我们不可能精确的干涉cpu调度线程。用法如下: ``` public class Test1 { public static void main(String[] args) throws InterruptedException { new MyThread("低级", 1).start(); new MyThread("中级", 5).start(); new MyThread("高级", 10).start(); } } class MyThread extends Thread { public MyThread(String name, int pro) { super(name);// 设置线程的名称 this.setPriority(pro);// 设置优先级 } @Override public void run() { for (int i = 0; i System.out.println(this.getName() + "线程第" + i + "次执行!"); if (i % 5 == 0) Thread.yield(); } } } ``` **注:**关于sleep()方法和yield()方的区别如下: ①、sleep方法暂停当前线程后,会进入阻塞状态,只有当睡眠时间到了,才会转入就绪状态。而yield方法调用后 ,是直接进入就绪状态,所以有可能刚进入就绪状态,又被调度到运行状态。 ②、sleep方法声明抛出了InterruptedException,所以调用sleep方法的时候要捕获该异常,或者显示声明抛出该异常。而yield方法则没有声明抛出任务异常。 ③、sleep方法比yield方法有更好的可移植性,通常不要依靠yield方法来控制并发线程的执行。
10.3.4.3. 线程合并——join
线程合并join: 线程的合并的含义就是将几个并行线程的线程合并为一个单线程执行,应用场景是当一个线程必须等待另一个线程执行完毕才能执行时,Thread类提供了join方法来完成这个功能,注意,它不是静态方法。 从上面的方法的列表可以看到,它有3个重载的方法: void join() 当前线程等该加入该线程后面,等待该线程终止。 void join(long millis) 当前线程等待该线程终止的时间最长为 millis 毫秒。 如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度 void join(long millis,int nanos) 等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。如果在millis时间内,该线程没有执行完,那么当前线程进入就绪状态,重新等待cpu调度
10.3.5. 死亡状态
当线程的run()方法执行完,或者被强制性地终止,就认为它死去。这个线程对象也许是活的,但是,它已经不是一个单独执行的线程。线程一旦死亡,就不能复生。 如果在一个死去的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。 线程调用 stop()方法或run()方法执行结束后,线程即处于终止状态。处于终止状态的线程不具有继续运行的能力。
10.4. 线程命名和获取
线程对象也是可以进行若干种操作的,而且所有的线程操作方法都在 Thread类中定义。 线程本身属于不可见的运行状态,即每次操作时是无法预料的,如果要想在程序中操作线程,唯一依靠的就是线程名称,而要想取得和设置线程的名称可以使用如下的方法。 1.方法: public Thread(Runable target,String naem) 类型:构造 描述:实例化线程对象,接受Runable接口子类对象,同时设置线程名称 2.方法:public final void setName(String name) 类型:普通 描述:设置线程名称 3.方法:public final String getName() 类型:普通 描述:取得线程名称 注意: 由于多线程的状态不确定,所以线程的名字就成为了唯一的分辨标记,则在定义线程名称时一定要在线程启动前设置,并且尽量不要重名,尽量不要为已经启动的线程修改名字。 由于线程的状态不确定,所以每次能够取得的线程对象,指的都是当前正在执行run()方法的线程,那么取得当前线程对象的方法。
10.5. 线程休眠sleep
线程休眠sleep: 线程的休眠指的是让程序的执行速度慢一些,在 Thread类中提供了以下的操作方法。 线程休眠: public static void sleep( long millis) throws Interrupted Exception,设置的休眠单位是毫秒。 --------------------------------------------- class MyThread implements Runable{ //线程的主体类 @Override public void run(){//线程主方法 for(int x=0;x try{ Thread.sleep(100); // 每次休眠100毫秒 }catch(InterruptedException e){ e.printStackTrace(); } sop(Thread.currentThread().getName()+"x="+x); } } } 启动多线程: public class TestDemo{ psvm(String[] args){ MyThread mt1= new MyThread("线程A");//创建线程对象 new Thread(mt1).start();//启动线程 } }
10.6. 设置线程的优先级
设置线程的优先级:相对优先级 每个线程执行时都有一个优先级的属性,优先级高的线程可以获得较多的执行机会,而优先级低的线程则获得较少的执行机会。与线程休眠类似,线程的优先级仍然无法保障线程的执行次序。只不过,优先级高的线程获取CPU资源的概率较大,优先级低的也并非没机会执行。 每个线程默认的优先级都与创建它的父线程具有相同的优先级,在默认情况下,main线程具有普通优先级。 注:Thread类提供了setPriority(int newPriority)和getPriority()方法来设置和返回一个指定线程的优先级,其中setPriority方法的参数是一个整数,范围是1~0之间,也可以使用Thread类提供的三个静态常量: ------------------------------------- MAX_PRIORITY =10 MIN_PRIORITY =1 NORM_PRIORITY =5 public class Test1 { public static void main(String[] args) throws InterruptedException { new MyThread("高级", 10).start(); new MyThread("低级", 1).start(); } } class MyThread extends Thread { public MyThread(String name,int pro) { super(name);//设置线程的名称 setPriority(pro);//设置线程的优先级 } @Override public void run() { for (int i = 0; i System.out.println(this.getName() + "线程第" + i + "次执行!"); } } } 注:虽然Java提供了10个优先级别,但这些优先级别需要操作系统的支持。不同的操作系统的优先级并不相同,而且也不能很好的和Java的10个优先级别对应。所以我们应该使用MAX_PRIORITY、MIN_PRIORITY和NORM_PRIORITY三个静态常量来设定优先级,这样才能保证程序最好的可移植性。 MAX_PRIORITY、10 MIN_PRIORITY、1 NORM_PRIORITY、5 public final void SetPriority(int newPriority()//设置线程优先级 public final int getPriority()//获取线程优先级
10.7. 后台(守护)线程
后台(守护)线程: 守护线程使用的情况较少,但并非无用,举例来说,JVM的垃圾回收、内存管理等线程都是守护线程。还有就是在做数据库应用时候,使用的数据库连接池,连接池本身也包含着很多后台线程,监控连接个数、超时时间、状态等等。调用线程对象的方法setDaemon(true),则可以将其设置为守护线程。守护线程的用途为: • 守护线程通常用于执行一些后台作业,例如在你的应用程序运行时播放背景音乐,在文字编辑器里做自动语法检查、自动保存等功能。 • Java的垃圾回收也是一个守护线程。守护线的好处就是你不需要关心它的结束问题。例如你在你的应用程序运行的时候希望播放背景音乐,如果将这个播放背景音乐的线程设定为非守护线程,那么在用户请求退出的时候,不仅要退出主线程,还要通知播放背景音乐的线程退出;如果设定为守护线程则不需要了。 setDaemon方法的详细说明: public final void setDaemon(boolean on) 将该线程标记为守护线程或用户线程。当正在运行的线程都是守护线程时,Java 虚拟机退出。 该方法必须在启动线程前调用。 该方法首先调用该线程的 checkAccess 方法,且不带任何参数。这可能抛出 SecurityException(在当前线程中)。 参数: on - 如果为 true,则将该线程标记为守护线程。 抛出: IllegalThreadStateException - 如果该线程处于活动状态。 SecurityException - 如果当前线程无法修改该线程。 注:JRE判断程序是否执行结束的标准是所有的前台执线程行完毕了,而不管后台线程的状态,因此,在使用后台县城时候一定要注意这个问题。 守护线程(用户进程)后台线程 也会抢夺CPU进程,与前台进程唯一的不同就是所有前台进程结束后,后台进程会自动结束。 setDaemon(Boolean on )
10.8. 正确结束进程
6、正确结束线程 Thread.stop()、Thread.suspend、Thread.resume、Runtime.runFinalizersOnExit这些终止线程运行的方法已经被废弃了,使用它们是极端不安全的!想要安全有效的结束一个线程,可以使用下面的方法: • 正常执行完run方法,然后结束掉; • 控制循环条件和判断条件的标识符来结束掉线程。 ---------------------------------------- class MyThread extends Thread { int i=0; boolean next=true; @Override public void run() { while (next) { if(i==10) next=false; i++; System.out.println(i); } } }