-
2019-10-29 11:49:21
一、 如果每个线程执行的代码相同
可以使用同一个
Runnable
对象,这个Runnable
对象中有那个共享数据,例如:卖票系统class Ticket implements Runnable{ private int tick = 20; Object obj = new Object(); public void run(){ while(true){ synchronized(obj){ if(tick>0){ //只能try,因为run是复写了Runnable接口的run,接口的run没有抛 try{Thread.sleep(100);}catch(Exception e){} //使用sleep不然执行每个线程都会占用完毕 System.out.println(Thread.currentThread().getName()+"....sale : "+ tick--); } } } } } class TicketDemo { public static void main(String[] args) { //只建立了一个Ticket对象,内存中只有一个tick成员变量,所以是共享数据 Ticket t = new Ticket(); Thread t1 = new Thread(t); Thread t2 = new Thread(t); Thread t3 = new Thread(t); Thread t4 = new Thread(t); t1.start(); t2.start(); t3.start(); t4.start(); } }
输出结果
Thread-0....sale : 20 Thread-1....sale : 19 ....... Thread-3....sale : 2 Thread-3....sale : 1
二、 如果每个线程执行的代码不同
1、具体实现
将共享数据封装在另外一个对象中,然后将这个对象逐一传递给各个
Runnable
对象。每个线程对共享数据的操作方法也分配到那个对象身上去完成,这样容易实现针对该数据进行的各个操作的互斥和通信。思想: 一个类提供数据和操作数据的同步方法,另外定义两个线程通过构造函数接收并操作数据,在主函数中直接创建线程对象,即可完成操作(可以实现两个内部类,不用构造方法传值,使用
final
定义data
局部变量)例如: 设计4个线程,其中两个线程每次对j增加1,另外两个线程每次对j减少1
public class MultyThreadShareMethod1 { public static void main(String[] args){ //将数据封装到一个对象上, ShareData2 data1 = new ShareData2(); //在runnable的构造函数中直接传入去操作 for(int i=0;i<2;i++){ new Thread(new MyRunnable1(data1)).start(); new Thread(new MyRunnable2(data1)).start(); } } } //封装共享数据和操作共享数据方法的类 class ShareData2{ private int j = 10; public synchronized void increment() { j++; System.out.println(Thread.currentThread().getName()+" inc : "+j); } public synchronized void decrement() { j--; System.out.println(Thread.currentThread().getName()+" dec : "+j); } } //增加的线程,需要传入一个共享数据 class MyRunnable1 implements Runnable { private ShareData2 data; public MyRunnable1(ShareData2 data) { this.data = data; } @Override public void run() { for(int i=0;i<10;i++){ data.increment(); } } } //减少的线程,需要传入一个共享数据 class MyRunnable2 implements Runnable { private ShareData2 data; public MyRunnable2(ShareData2 data) { this.data = data; } @Override public void run() { for(int i=0;i<10;i++){ data.decrement(); } } }
输出结果
Thread-0 inc : 11 ... Thread-1 dec : 10
2、 技巧总结
要同步互斥的几段代码最好是分别放在几个独立的方法中,这些方法再放在同一个类中,这样比较容易实现它们之间的同步互斥或通信。极端且简单的方式,即在任意一个类中定义一个
static
的变量,这将被所有线程共享。本文都来自博客多个线程之间共享数据的方式探讨的精简版,详细的分析参考博客
更多相关内容 -
Java多线程编程之ThreadLocal线程范围内的共享变量
2020-09-03 17:21:03主要介绍了Java多线程编程之ThreadLocal线程范围内的共享变量,本文讲解了ThreadLocal的作用和目的、ThreadLocal的应用场景、ThreadLocal的使用实例等,需要的朋友可以参考下 -
Java多线程变量共享与隔离
2021-11-12 11:38:42线程相关 线程的相关API Thread.currentThread().getName():获取当前线程的名字 start():1.启动当前线程2.调用线程中的run方法 run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中 ...线程相关
线程的相关API
- Thread.currentThread().getName():获取当前线程的名字
- start():1.启动当前线程2.调用线程中的run方法
- run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
- currentThread():静态方法,返回执行当前代码的线程
- getName():获取当前线程的名字
- setName():设置当前线程的名字
- yield():主动释放当前线程的执行权
- join():在线程中插入执行另一个线程,该线程被阻塞,直到插入执行的线程完全执行完毕以后,该线程才继续执行下去
- stop():过时方法。当执行此方法时,强制结束当前线程。
- sleep(long millitime):线程休眠一段时间
- isAlive():判断当前线程是否存活
线程的调度
调度策略:
- 时间片:线程的调度采用时间片轮转的方式
- 抢占式:高优先级的线程抢占CPU
Java的调度方法:
- 对于同优先级的线程组成先进先出队列(先到先服务),使用时间片策略
- 对高优先级,使用优先调度的抢占式策略
线程的优先级
等级:
MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5方法:
getPriority():返回线程优先级
setPriority(int newPriority):改变线程的优先级注意事项:高优先级的线程要抢占低优先级的线程的cpu的执行权。但是仅是从概率上来说的,高优先级的线程更有可能被执行。并不意味着只有高优先级的线程执行完以后,低优先级的线程才执行。
方法和变量的线程安全问题
静态方法
与静态成员变量一样,属于类本身,在类装载的时候被装载到内存(Memory),不自动进行销毁,会一直存在于内存中,直到JVM关闭。
非静态方法
又叫实例化方法,属于实例对象,实例化后才会分配内存,必须通过类的实例来引用。不会常驻内存,当实例对象被JVM 回收之后,也跟着消失。
静态变量
线程非安全,静态变量即类变量,位于方法区,为所有该类下的对象共享,共享一份内存,一旦静态变量被修改,其他对象均对修改可见,故线程非安全。
实例变量
实例变量为对象实例私有,在虚拟机的堆中分配,若在系统中只存在一个此对象的实例,在多线程环境下,“犹如”静态变量那样,被某个线程修改后,其他线程对修改均可见,故线程非安全;如果每个线程执行都是在不同的对象中,那对象与对象之间的实例变量的修改将互不影响,故线程安全。
局部变量
线程安全,每个线程执行时将会把局部变量放在各自栈帧的工作内存中,线程间不共享,故不存在线程安全问题。
变量共享
共享变量线程安全问题
可见性
如果一个线程对共享变量值的修改,能够及时的被其他线程看到,叫做共享变量的可见性。如果一个变量同时在多个线程的工作内存中存在副本,那么这个变量就叫共享变量。
Java多线程里对于共享变量的操作往往需要考虑进行一定的同步互斥操作,原来是因为Java内存模型导致的共享内存对于线程不可见。
Java 内存模型规定,将所有的变量都存放在主内存中。
- 线程对共享变量的所有操作必须在工作内存中进行,不能直接操作主内存。
- 不同线程间不能访问彼此的工作内存中的变量,线程间变量值的传递都必须经过主内存。
多个线程同时对主内存的一个共享变量进行读取和修改时,首先会读取这个变量到自己的工作内存中成为一个副本,对这个副本进行改动之后,再更新回主内存中变量所在的地方。由于CPU时间片是以线程为最小单位,所以这里的工作内存实际上就是指的物理缓存,CPU运算时获取数据的地方;而主内存也就是指的是内存,也就是原始的共享变量存放的位置。
一个线程A对共享变量1的修改对线程B可见,需要经过下列步骤:
- 线程A将更改变量1后的值更新到主内存
- 主内存将更新后的变量1的值更新到线程B的工作内存中变量1的副本
要实现共享变量的可见性必须保证下列两点:
- 线程对工作内存中副本的更改能够及时的更新到主内存上
- 其他线程能够及时的将主内存上共享变量的更新刷新到自己工作内存的该变量的副本上
可见性举例
一个双核 CPU 系统架构,每个核有自己的控制器和运算器,其中控制器包含一组寄存器和操作控制器,运算器执行算术逻辅运算。CPU的每个核都有自己的一级缓存,在有些架构里面还有一个所有CPU都共享的二级缓存。
1、线程A首先获取共享变量X的值,由于两级Cache都没有命中,所以加载主内存中X的值,假如为0。然后把X=0的值缓存到两级缓存,线程A修改X的值为1,然后将其写入两级Cache,并且刷新到主内存。线程A操作完毕后,线程A所在的CPU的两级Cache内和主内存里面的X的值都是l。2、线程B获取X的值,首先一级缓存没有命中,然后看二级缓存,二级缓存命中了,所以返回X=1;到这里一切都是正常的,因为这时候主内存中也是X=l。然后线程B修改X的值为2,并将其存放到线程2所在的一级Cache和共享二级Cache中,最后更新主内存中X的值为2,到这里一切都是好的。
3、线程A这次又需要修改X的值,获取时一级缓存命中,并且X=l这里问题就出现了,明明线程B已经把X的值修改为2,为何线程A获取的还是l呢?这就是共享变量的内存不可见问题,也就是线程B写入的值对线程A不可见。
共享变量可见性的实现
Java中可以通过synchronized、volatile、java concurrent类来实现共享变量的可见性。
synchronized
使用synchronized可以保证原子性(synchronized代码块内容要么不执行,要执行就保证全部执行完毕)和可见性,修改后的代码为在write和read方法上加synchronized关键字。
JMM关于Synchronized的两条规定:
- 线程解锁前,必须把共享变量的最新值刷新到主内存中;
- 线程加锁时,将清空工作内存中共享变量的值,从而使用共享变量是需要从主内存中重新读取最新的值(加锁与解锁需要统一把锁)。
synchronized 实际上是对访问修改共享变量的代码块进行加互斥锁,多个线程对synchronized代码块的访问时,某一时刻仅仅有一个线程在访问和修改代码块中的内容(加锁),其他所有的线程等待该线程离开代码块时(释放锁)才有机会进入synchronized代码块。
某一个线程进入synchronized代码块前后,执行过程入如下:
- 线程获得互斥锁
- 清空工作内存
- 从主内存拷贝共享变量最新的值到工作内存成为副本
- 执行代码
- 将修改后的副本的值刷新回主内存中
- 线程释放锁
随后,其他代码在进入synchronized代码块的时候,所读取到的工作内存上共享变量的值都是上一个线程修改后的最新值。
注意,synchronized加锁后用到的变量才会从主内存拉取、才会修改后刷新回主内存。
举例说明:
该代码程序不会进入死循环,分析执行过程:- 创建0对象;
- 创建新线程,异步执行;
- 执行while,此时isStop=false;进入循环;
- 遇到synchronized锁,得到锁;
- 清空主线程的工作内存;
从主内存拷贝共享变量最新的值到工作内存成为副本(该步骤省略,因为所里面没有代码块(不知道是否可以这么理解));
执行代码(该步骤省略,因为没有代码);
将修改后的副本的值刷新会主内存中(该步骤省略,因为没有代码); - 释放锁
- 重新执行while,此时工作内存中没有isStop,从主内存中获取最新值,如果isStop=false,继续执行4、5、6、7步骤,知道2秒钟后子线程将isStop值改为true,并刷新回主线程,此时步骤7从主内存中获得的isStop=true,结束while循环,主线程结束。
特别情况:
在上个例子的基础上加上一段代码:System.out.println(isStop),查看打印日志:
没有达到自己预期的效果:结束前一次貌似应该打印true;
上面代码其实有两种结果:- 最后一次打印isStop为true(几率小)
- 最后一次打印isStop为false(几率大)
具体原因:
System.out.println(boolean x)方法其实也含有一个synchronized锁。
结果1出现的情况:where时,isStop=false,第一个锁o对象锁清空工作内存后重新从内存得到isStop=true(几率小)
结果2出现的情况:where时,isStop=false,第一个锁o对象锁清空工作内存后重新从内存得到isStop=false,println得到的参数值为false(值传递),println的锁PrintStream对象锁清空工作内存,再次where时,isStop=true(几率大)。
volatile
volatile变量每次被线程访问时,都强迫线程从主内存中重读该变量的最新值,而当该变量发生修改变化时,也会强迫线程将最新的值刷新回主内存。这样一来,不同的线程都能及时的看到该变量的最新值。
但是volatile不能保证变量更改的原子性
比如number++,这个操作实际上是三个操作的集合(读取number,number加1,将新的值写回number),volatile只能保证每一 步的操作对所有线程是可见的,但是假如两个线程都需要执行number++,那么这一共6个操作集合,之间是可能会交叉执行的,那么最后导致number 的结果可能会不是所期望的。举例说明:
陈序不会进入死循环,原因:
isStop是被volatile修饰的,所有每次while时都是从主内存中获取isStop的值,当子线程2秒钟后修改了isStop的值为true,并刷新进了住内存,此后while从主内存中获取的isStop值为true,结束循环。AtomicInteger:一个提供原子操作的Integer的类。在Java语言中,++i和i++操作并不是线程安全的,在使用的时候,不可避免的会用到synchronized关键字。而AtomicInteger则通过一种线程安全的加减操作接口。
synchronized和volatile比较
-
volatile不需要同步操作,所以效率更高,不会阻塞线程,但是适用情况比较窄
-
volatile读变量相当于加锁(即进入synchronized代码块),而写变量相当于解锁(退出synchronized代码块)
-
synchronized既能保证共享变量可见性,也可以保证锁内操作的原子性;volatile只能保证可见性
volatile适用情况
-
对变量的写入操作不依赖当前值
比如自增自减、number = number + 5等(不满足) -
当前volatile变量不依赖于别的volatile变量
比如 volatile_var > volatile_var2这个不等式(不满足)
特殊操作会从主内存中拉取值
- 变量被赋值
- 变量参与计算(比如加减乘除)
- 字符串拼接(验证过)
- ++、–等操作
- 线程休眠(验证过)
以上操作会从主内存拉去值到工作内存,所有如果在上面的例子的weile循环中有这些操作,不会造成死循环。
变量隔离
多线程之间就是因为数据共享在多个线程才导致了线程不安全,这就要求线程间的数据需要隔离,从根本上解决了线程安全问题。
ThreadLocal
提供线程局部变量;一个线程局部变量在多个线程中,分别有独立的值(副本)。
使用ThreadLocal的好处
- 达到线程安全
- 不需要加锁,提高执行效率
- 更高效地利用内存节省开销,用ThreadLocal可以节省内存和开销。
- 免去传参的繁琐,不需要每次都传同样的参数,ThreadLocal使得代码耦合度更低,更优雅
ThreadLocal主要方法
主要是initialValue、set、get、remove这几个方法
- initialValue方法返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用get的时候,才会触发。
- 当线程第一次使用get方法访问变量时,将调用initialValue方法,除非线程先前调用了set方法,在这种情况下,不会为线程调用本initialValue方法。
- 通常,每个线程最多调用一次initialValue()方法,但如果已经调用了一次remove()后,再调用get(),则可以再次调用initialValue(),相当于第一次调用get()。
- 如果不重写initialValue()方法,这个方法会返回null。一般使用匿名内部类的方法来重写initialValue()方法,以便在后续使用中可以初始化副本对象。
ThreadLocal源码分析
Thread、ThreadLocal、ThreadLocalMap三者的关系:
每个Thread对象都有一个ThreadLocalMap,每个ThreadLocalMap可以存储多个ThreadLocal。get方法
get方法是先取出当前线程的ThreadLocalMap,然后调用map.getEntry方法,把本ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocal的value。注意:这个map以及map中的key和value都是保存在线程中ThreadLocalMap的,而不是保存在ThreadLocal中
getMap方法:
获取到当前线程内的ThreadLocalMap对象。每个线程内都有ThreadLocalMap对象,名为threadLocals,初始值为null。set方法
把当前线程需要全局共享的value传入initialValue方法
这个方法没有默认实现,如果要用initialValue方法,需要自己实现,通常使用匿名内部类的方式实现remove方法
删除对应这个线程的值。ThreadLocalMap类
ThreadLocalMap类,也就是Thread.threadLocals。// 此行声明在Thread类中,创建ThreadLocalMap就是对Thread类的这个成员变量赋值 ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap 类是每个线程Thread类里面的变量,但ThreadLocalMap这个静态内部类定义在ThreadLocal类中,其中发现这一行代码
private Entry[] table;
里面最重要的是一个键值对数组Entry[] table,可以认为是一个map,键值对:
- 键:这个ThreadLocal
- 值:实际需要的成员变量,比如User或者SimpleDateFormat对象
这个思路和HashMap一样,那么我们可以把它想象成HashMap来分析,但是实现上略有不同。
比如处理冲突方式不同,HashMap采用链地址法,而ThreadLocalMap采用的是线性探测法,也就是如果发生冲突,就继续找下一个空位置,而不是用链表拉链
通过源码分析可以看出,setInitialValue和直接set最后都是利用map.set()方法来设置值,最后都会对应到ThreadLocalMap的一个Entry
ThreadLocal注意事项
1.ThreadLocal内存泄漏问题
ThreadLocalMap中的Entry继承自 WeakReference,是弱引用,ThreadLocal可能出现Value泄漏!什么是内存泄漏:某个对象不再有用,但是占用的内存却不能被回收
弱引用:通过WeakReference类实现的,在GC的时候,不管内存空间足不足都会回收这个对象,适用于内存敏感的缓存,ThreadLocal中的key就用到了弱引用,有利于内存回收。
强引用:我们平日里面的用到的new了一个对象就是强引用,例如 Object obj = new Object();当JVM的内存空间不足时,宁愿抛出OutOfMemoryError使得程序异常终止也不愿意回收具有强引用的存活着的对象。ThreadLocalMap 的每个 Entry 都是一个对key的弱引用,同时,每个 Entry 都包含了一个对value的强引用,如下:
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); // key值给WeakReference处理 value = v; // value直接用变量保存,是强引用 } }
正常情况下,当线程终止,保存在ThreadLocalMap里的value会被垃圾回收,因为没有任何强引用了。但如果线程不终止(比如线程需要保持很久),那么key对应的value就不能被回收,因为有以下的调用链:
Thread---->ThreadLocalMap---->Entry(key为null,弱引用被回收)---->value
因为value和Thread之间还存在这个强引用链路,所以导致value无法回收,就可能会出现OOM。
JDK已经考虑到了这个问题,所以在set, remove, rehash方法中会扫描key为null的Entry,并把对应的value设置为null,这样value对象就可以被回收。比如rehash里面调用resize,如果key回收了,那么value也设置为null,断开强引用链路,便于垃圾回收。
private void resize() { ......省略代码 ThreadLocal<?> k = e.get(); if (k == null) { e.value = null; // Help the GC } ......
但是如果一个ThreadLocal不被使用,那么实际上set, remove, rehash方法也不会被调用,如果同时线程又不停止,那么调用链就一直存在,那么就导致了value的内存泄漏。
ThreadLocal如何避免内存泄漏
及时调用remove方法,就会删除对应的Entry对象,可以避免内存remove泄漏,所以使用完ThreadLocal之后,应该调用remove方法。
比如拦截器获取到用户信息,用户信息存在ThreadLocalMap中,线程请求结束之前拦住它,并用remove清除User对象,这样就能稳妥的保证不会内存泄漏。注意事项
- 共享对象问题
如果在每个线程中ThreadLocal.set()进去的东西本来就是多线程共享的同一个对象,比如static对象,那么多个线程的ThreadLocal.get()取得的还是这个共享对象本身,还是有并发访问问题。 - 不要强行使用ThreadLocal
如果可以不使用ThreadLocal就能解决问题,那么不要强行使用,在任务数很少的时候,可以通过在局部变量中新建对象解决。 - 优先使用框架的支持,而不是自己创造
在Spring中,如果可以使用RequestContextHolder,那么就不需要自己维护ThreadLocal,因为自己可能会忘记调用remove()方法等,造成内存泄漏。
参考文章:
Java多线程里共享变量线程安全问题的原因
Java多线程共享变量控制
Java多线程超详解
ThreadLocal详解 -
Java多线程超详解
2019-06-11 01:00:30随着计算机的配置越来越高,我们需要将进程进一步优化,细分为线程,充分提高图形化界面的多线程的开发。这就要求对线程的掌握很彻底。 那么话不多说,今天本帅将记录自己线程的学习。 线程的相关API //获取当前...引言
随着计算机的配置越来越高,我们需要将进程进一步优化,细分为线程,充分提高图形化界面的多线程的开发。这就要求对线程的掌握很彻底。
那么话不多说,今天本帅将记录自己线程的学习。程序,进程,线程的基本概念+并行与并发:
程序:是为完成特定任务,用某种语言编写的一组指令的集合,即指一段静态的代码,静态对象。
进程:是程序的一次执行过程,或是正在运行的一个程序,是一个动态的过程,有它自身的产生,存在和消亡的过程。-------生命周期
线程:进程可进一步细化为线程,是一个程序内部的一条执行路径即:线程《线程(一个程序可以有多个线程)
程序:静态的代码 进程:动态执行的程序
线程:进程中要同时干几件事时,每一件事的执行路径成为线程。并行:多个CPU同时执行多个任务,比如:多个人同时做不同的事
并发:一个CPU(采用时间片)同时执行多个任务,比如秒杀平台,多个人做同件事线程的相关API
//获取当前线程的名字
Thread.currentThread().getName()1.start():1.启动当前线程2.调用线程中的run方法
2.run():通常需要重写Thread类中的此方法,将创建的线程要执行的操作声明在此方法中
3.currentThread():静态方法,返回执行当前代码的线程
4.getName():获取当前线程的名字
5.setName():设置当前线程的名字
6.yield():主动释放当前线程的执行权
7.join():在线程中插入执行另一个线程,该线程被阻塞,直到插入执行的线程完全执行完毕以后,该线程才继续执行下去
8.stop():过时方法。当执行此方法时,强制结束当前线程。
9.sleep(long millitime):线程休眠一段时间
10.isAlive():判断当前线程是否存活判断是否是多线程
一条线程即为一条执行路径,即当能用一条路径画出来时即为一个线程
例:如下看似既执行了方法一,又执行了方法2,但是其实质就是主线程在执行方法2和方法1这一条路径,所以就是一个线程public class Sample{ public void method1(String str){ System.out.println(str); } public void method2(String str){ method1(str); } public static void main(String[] args){ Sample s = new Sample(); s.method2("hello"); } }
线程的调度
调度策略:
时间片:线程的调度采用时间片轮转的方式
抢占式:高优先级的线程抢占CPU
Java的调度方法:
1.对于同优先级的线程组成先进先出队列(先到先服务),使用时间片策略
2.对高优先级,使用优先调度的抢占式策略线程的优先级
等级:
MAX_PRIORITY:10
MIN_PRIORITY:1
NORM_PRIORITY:5方法:
getPriority():返回线程优先级
setPriority(int newPriority):改变线程的优先级注意!:高优先级的线程要抢占低优先级的线程的cpu的执行权。但是仅是从概率上来说的,高优先级的线程更有可能被执行。并不意味着只有高优先级的线程执行完以后,低优先级的线程才执行。
多线程的创建方式
1. 方式1:继承于Thread类
1.创建一个集成于Thread类的子类 (通过ctrl+o(override)输入run查找run方法)
2.重写Thread类的run()方法
3.创建Thread子类的对象
4.通过此对象调用start()方法start与run方法的区别:
start方法的作用:1.启动当前线程 2.调用当前线程的重写的run方法(在主线程中生成子线程,有两条线程)
调用start方法以后,一条路径代表一个线程,同时执行两线程时,因为时间片的轮换,所以执行过程随机分配,且一个线程对象只能调用一次start方法。
run方法的作用:在主线程中调用以后,直接在主线程一条线程中执行了该线程中run的方法。(调用线程中的run方法,只调用run方法,并不新开线程)总结:我们不能通过run方法来新开一个线程,只能调用线程中重写的run方法(可以在线程中不断的调用run方法,但是不能开启子线程,即不能同时干几件事),start是开启线程,再调用方法(即默认开启一次线程,调用一次run方法,可以同时执行几件事)
多线程例子(火车站多窗口卖票问题)
package com.example.paoduantui.Thread; import android.view.Window; /** * * 创建三个窗口卖票,总票数为100张,使用继承自Thread方式 * 用静态变量保证三个线程的数据独一份 * * 存在线程的安全问题,有待解决 * * */ public class ThreadDemo extends Thread{ public static void main(String[] args){ window t1 = new window(); window t2 = new window(); window t3 = new window(); t1.setName("售票口1"); t2.setName("售票口2"); t3.setName("售票口3"); t1.start(); t2.start(); t3.start(); } } class window extends Thread{ private static int ticket = 100; //将其加载在类的静态区,所有线程共享该静态变量 @Override public void run() { while(true){ if(ticket>0){ // try { // sleep(100); // } catch (InterruptedException e) { // e.printStackTrace(); // } System.out.println(getName()+"当前售出第"+ticket+"张票"); ticket--; }else{ break; } } } }
2. 方式2:实现Runable接口方式
1.创建一个实现了Runable接口的类
2.实现类去实现Runnable中的抽象方法:run()
3.创建实现类的对象
4.将此对象作为参数传递到Thread类中的构造器中,创建Thread类的对象
5.通过Thread类的对象调用start()具体操作,将一个类实现Runable接口,(插上接口一端)。
另外一端,通过实现类的对象与线程对象通过此Runable接口插上接口实现package com.example.paoduantui.Thread; public class ThreadDemo01 { public static void main(String[] args){ window1 w = new window1(); //虽然有三个线程,但是只有一个窗口类实现的Runnable方法,由于三个线程共用一个window对象,所以自动共用100张票 Thread t1=new Thread(w); Thread t2=new Thread(w); Thread t3=new Thread(w); t1.setName("窗口1"); t2.setName("窗口2"); t3.setName("窗口3"); t1.start(); t2.start(); t3.start(); } } class window1 implements Runnable{ private int ticket = 100; @Override public void run() { while(true){ if(ticket>0){ // try { // sleep(100); // } catch (InterruptedException e) { // e.printStackTrace(); // } System.out.println(Thread.currentThread().getName()+"当前售出第"+ticket+"张票"); ticket--; }else{ break; } } } }
比较创建线程的两种方式:
开发中,优先选择实现Runable接口的方式
原因1:实现的方式没有类的单继承性的局限性
2:实现的方式更适合用来处理多个线程有共享数据的情况
联系:Thread也是实现自Runable,两种方式都需要重写run()方法,将线程要执行的逻辑声明在run中3.新增的两种创建多线程方式
1.实现callable接口方式:
与使用runnable方式相比,callable功能更强大些:
runnable重写的run方法不如callaalbe的call方法强大,call方法可以有返回值
方法可以抛出异常
支持泛型的返回值
需要借助FutureTask类,比如获取返回结果package com.example.paoduantui.Thread; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; /** * 创建线程的方式三:实现callable接口。---JDK 5.0新增 *是否多线程?否,就一个线程 * * 比runable多一个FutureTask类,用来接收call方法的返回值。 * 适用于需要从线程中接收返回值的形式 * * //callable实现新建线程的步骤: * 1.创建一个实现callable的实现类 * 2.实现call方法,将此线程需要执行的操作声明在call()中 * 3.创建callable实现类的对象 * 4.将callable接口实现类的对象作为传递到FutureTask的构造器中,创建FutureTask的对象 * 5.将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start方法启动(通过FutureTask的对象调用方法get获取线程中的call的返回值) * * */ //实现callable接口的call方法 class NumThread implements Callable{ private int sum=0;// //可以抛出异常 @Override public Object call() throws Exception { for(int i = 0;i<=100;i++){ if(i % 2 == 0){ System.out.println(Thread.currentThread().getName()+":"+i); sum += i; } } return sum; } } public class ThreadNew { public static void main(String[] args){ //new一个实现callable接口的对象 NumThread numThread = new NumThread(); //通过futureTask对象的get方法来接收futureTask的值 FutureTask futureTask = new FutureTask(numThread); Thread t1 = new Thread(futureTask); t1.setName("线程1"); t1.start(); try { //get返回值即为FutureTask构造器参数callable实现类重写的call的返回值 Object sum = futureTask.get(); System.out.println(Thread.currentThread().getName()+":"+sum); } catch (ExecutionException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } }
使用线程池的方式:
背景:经常创建和销毁,使用量特别大的资源,比如并发情况下的线程,对性能影响很大。
思路:提前创建好多个线程,放入线程池之,使用时直接获取,使用完放回池中。可以避免频繁创建销毁,实现重复利用。类似生活中的公共交通工具。(数据库连接池)
好处:提高响应速度(减少了创建新线程的时间)
降低资源消耗(重复利用线程池中线程,不需要每次都创建)
便于线程管理
corePoolSize:核心池的大小
maximumPoolSize:最大线程数
keepAliveTime:线程没有任务时最多保持多长时间后会终止
。。。。。。JDK 5.0 起提供了线程池相关API:ExecutorService 和 Executors
ExecutorService:真正的线程池接口。常见子类ThreadPoolExecutor.
void execute(Runnable coommand):执行任务/命令,没有返回值,一般用来执行Runnable
Futuresubmit(Callable task):执行任务,有返回值,一般又来执行Callable
void shutdown():关闭连接池。Executors 工具类,线程池的工厂类,用于创建并返回不同类型的线程池 Executors.newCachedThreadPool() 创建一个可根据需要创建新线程的线程池 Executors.newFixedThreadPool(n) 创建一个可重用固定线程数的线程池 Executors.newSingleThreadExecutor() :创建一个只有一个线程的线程池 Executors.newScheduledThreadPool(n) 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。 线程池构造批量线程代码如下:
package com.example.paoduantui.Thread; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * 创建线程的方式四:使用线程池(批量使用线程) *1.需要创建实现runnable或者callable接口方式的对象 * 2.创建executorservice线程池 * 3.将创建好的实现了runnable接口类的对象放入executorService对象的execute方法中执行。 * 4.关闭线程池 * * */ class NumberThread implements Runnable{ @Override public void run() { for(int i = 0;i<=100;i++){ if (i % 2 ==0 ) System.out.println(Thread.currentThread().getName()+":"+i); } } } class NumberThread1 implements Runnable{ @Override public void run() { for(int i = 0;i<100; i++){ if(i%2==1){ System.out.println(Thread.currentThread().getName()+":"+i); } } } } public class ThreadPool { public static void main(String[] args){ //创建固定线程个数为十个的线程池 ExecutorService executorService = Executors.newFixedThreadPool(10); //new一个Runnable接口的对象 NumberThread number = new NumberThread(); NumberThread1 number1 = new NumberThread1(); //执行线程,最多十个 executorService.execute(number1); executorService.execute(number);//适合适用于Runnable //executorService.submit();//适合使用于Callable //关闭线程池 executorService.shutdown(); } }
目前两种方式要想调用新线程,都需要用到Thread中的start方法。
java virtual machine(JVM):java虚拟机内存结构
程序(一段静态的代码)——————》加载到内存中——————》进程(加载到内存中的代码,动态的程序)
进程可细分为多个线程,一个线程代表一个程序内部的一条执行路径
每个线程有其独立的程序计数器(PC,指导着程序向下执行)与运行栈(本地变量等,本地方法等)
大佬传送门:https://blog.csdn.net/bluetjs/article/details/52874852
线程通信方法:
wait()/ notify()/ notifayAll():此三个方法定义在Object类中的,因为这三个方法需要用到锁,而锁是任意对象都能充当的,所以这三个方法定义在Object类中。
由于wait,notify,以及notifyAll都涉及到与锁相关的操作
wait(在进入锁住的区域以后阻塞等待,释放锁让别的线程先进来操作)---- Obj.wait 进入Obj这个锁住的区域的线程把锁交出来原地等待通知
notify(由于有很多锁住的区域,所以需要将区域用锁来标识,也涉及到锁) ----- Obj.notify 新线程进入Obj这个区域进行操作并唤醒wait的线程有点类似于我要拉粑粑,我先进了厕所关了门,但是发现厕所有牌子写着不能用,于是我把厕所锁给了别人,别人进来拉粑粑还是修厕所不得而知,直到有人通知我厕所好了我再接着用。
所以wait,notify需要使用在有锁的地方,也就是需要用synchronize关键字来标识的区域,即使用在同步代码块或者同步方法中,且为了保证wait和notify的区域是同一个锁住的区域,需要用锁来标识,也就是锁要相同的对象来充当
线程的分类:
java中的线程分为两类:1.守护线程(如垃圾回收线程,异常处理线程),2.用户线程(如主线程)
若JVM中都是守护线程,当前JVM将退出。(形象理解,唇亡齿寒)
线程的生命周期:
JDK中用Thread.State类定义了线程的几种状态,如下:
线程生命周期的阶段 描述 新建 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态 就绪 处于新建状态的线程被start后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,只是没分配到CPU资源 运行 当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能 阻塞 在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时终止自己的执行,进入阻塞状态 死亡 线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束 线程的同步:在同步代码块中,只能存在一个线程。
线程的安全问题:
什么是线程安全问题呢?
线程安全问题是指,多个线程对同一个共享数据进行操作时,线程没来得及更新共享数据,从而导致另外线程没得到最新的数据,从而产生线程安全问题。上述例子中:创建三个窗口卖票,总票数为100张票
1.卖票过程中,出现了重票(票被反复的卖出,ticket未被减少时就打印出了)错票。
2.问题出现的原因:当某个线程操作车票的过程中,尚未完成操作时,其他线程参与进来,也来操作车票。(将此过程的代码看作一个区域,当有线程进去时,装锁,不让别的线程进去)
生动理解的例子:有一个厕所,有人进去了,但是没有上锁,于是别人不知道你进去了,别人也进去了对厕所也使用造成错误。
3.如何解决:当一个线程在操作ticket时,其他线程不能参与进来,直到此线程的生命周期结束
4.在java中,我们通过同步机制,来解决线程的安全问题。方式一:同步代码块
使用同步监视器(锁)
Synchronized(同步监视器){
//需要被同步的代码
}
说明:- 操作共享数据的代码(所有线程共享的数据的操作的代码)(视作卫生间区域(所有人共享的厕所)),即为需要共享的代码(同步代码块,在同步代码块中,相当于是一个单线程,效率低)
- 共享数据:多个线程共同操作的数据,比如公共厕所就类比共享数据
- 同步监视器(俗称:锁):任何一个的对象都可以充当锁。(但是为了可读性一般设置英文成lock)当锁住以后只能有一个线程能进去(要求:多个线程必须要共用同一把锁,比如火车上的厕所,同一个标志表示有人)
Runable天生共享锁,而Thread中需要用static对象或者this关键字或者当前类(window。class)来充当唯一锁
方式二:同步方法
使用同步方法,对方法进行synchronized关键字修饰
将同步代码块提取出来成为一个方法,用synchronized关键字修饰此方法。
对于runnable接口实现多线程,只需要将同步方法用synchronized修饰
而对于继承自Thread方式,需要将同步方法用static和synchronized修饰,因为对象不唯一(锁不唯一)总结:1.同步方法仍然涉及到同步监视器,只是不需要我们显示的声明。
2.非静态的同步方法,同步监视器是this
静态的同步方法,同步监视器是当前类本身。继承自Thread。class方式三:JDK5.0新增的lock锁方法
package com.example.paoduantui.Thread; import java.util.concurrent.locks.ReentrantLock; class Window implements Runnable{ private int ticket = 100;//定义一百张票 //1.实例化锁 private ReentrantLock lock = new ReentrantLock(); @Override public void run() { while (true) { //2.调用锁定方法lock lock.lock(); if (ticket > 0) { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "售出第" + ticket + "张票"); ticket--; } else { break; } } } } public class LockTest { public static void main(String[] args){ Window w= new Window(); Thread t1 = new Thread(w); Thread t2 = new Thread(w); Thread t3 = new Thread(w); t1.setName("窗口1"); t2.setName("窗口1"); t3.setName("窗口1"); t1.start(); t2.start(); t3.start(); } }
总结:Synchronized与lock的异同?
相同:二者都可以解决线程安全问题
不同:synchronized机制在执行完相应的代码逻辑以后,自动的释放同步监视器
lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock())(同时以为着lock的方式更为灵活)优先使用顺序:
LOCK-》同步代码块-》同步方法判断线程是否有安全问题,以及如何解决:
1.先判断是否多线程
2.再判断是否有共享数据
3.是否并发的对共享数据进行操作
4.选择上述三种方法解决线程安全问题例题:
package com.example.paoduantui.Thread; /*** * 描述:甲乙同时往银行存钱,存够3000 * * * */ //账户 class Account{ private double balance;//余额 //构造器 public Account(double balance) { this.balance = balance; } //存钱方法 public synchronized void deposit(double amt){ if(amt>0){ balance +=amt; try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+"存钱成功,余额为:"+balance); } } } //两个顾客线程 class Customer extends Thread{ private Account acct; public Customer(Account acct){ this.acct = acct; } @Override public void run() { for (int i = 0;i<3;i++){ acct.deposit(1000); } } } //主方法,之中new同一个账户,甲乙两个存钱线程。 public class AccountTest { public static void main(String[] args){ Account acct = new Account(0); Customer c1 = new Customer(acct); Customer c2 = new Customer(acct); c1.setName("甲"); c2.setName("乙"); c1.start(); c2.start(); } }
解决单例模式的懒汉式的线程安全问题:
单例:只能通过静态方法获取一个实例,不能通过构造器来构造实例
1.构造器的私有化:
private Bank(){}//可以在构造器中初始化东西
private static Bank instance = null;//初始化静态实例public static Bank getInstance(){
if(instance!=null){
instance = new Bank();
}
return instance;
}假设有多个线程调用此单例,而调用的获取单例的函数作为操作共享单例的代码块并没有解决线程的安全问题,会导致多个线程都判断实例是否为空,此时就会导致多个实例的产生,也就是单例模式的线程安全问题。
解决线程安全问题的思路:
- 将获取单例的方法改写成同部方法,即加上synchronized关键字,此时同步监视器为当前类本身。(当有多个线程并发的获取实例时,同时只能有一个线程获取实例),解决了单例模式的线程安全问题。
- 用同步监视器包裹住同步代码块的方式。
懒汉式单例模式的模型,例如:生活中的限量版的抢购:
当一群人并发的抢一个限量版的东西的时候,可能同时抢到了几个人,他们同时进入了房间(同步代码块内)
但是只有第一个拿到限量版东西的人才能到手,其余人都不能拿到,所以效率稍高的做法是,当东西被拿走时,我们在门外立一块牌子,售罄。
这样就减少了线程的等待。即下面效率稍高的懒汉式写法:package com.example.paoduantui.Thread; public class Bank { //私有化构造器 private Bank(){} //初始化静态实例化对象 private static Bank instance = null; //获取单例实例,此种懒汉式单例模式存在线程不安全问题(从并发考虑) public static Bank getInstance(){ if(instance==null){ instance = new Bank(); } return instance; } //同步方法模式的线程安全 public static synchronized Bank getInstance1(){ if(instance==null){ instance = new Bank(); } return instance; } //同步代码块模式的线程安全(上锁) public static Bank getInstance2(){ synchronized (Bank.class){ if(instance==null){ instance = new Bank(); } return instance; } } //效率更高的线程安全的懒汉式单例模式 /** * 由于当高并发调用单例模式的时候,类似于万人夺宝,只有第一个进入房间的人才能拿到宝物, * 当多个人进入这个房间时,第一个人拿走了宝物,也就另外几个人需要在同步代码块外等候, * 剩下的人只需要看到门口售罄的牌子即已知宝物已经被夺,可以不用进入同步代码块内,提高了效率。 * * * */ public static Bank getInstance3(){ if (instance==null){ synchronized (Bank.class){ if(instance==null){ instance = new Bank(); } } } return instance; } }
线程的死锁问题:
线程死锁的理解:僵持,谁都不放手,一双筷子,我一只你一只,都等对方放手(死锁,两者都进入阻塞,谁都吃不了饭,进行不了下面吃饭的操作)
出现死锁以后,不会出现提示,只是所有线程都处于阻塞状态,无法继续package com.example.paoduantui.Thread; /** * 演示线程的死锁问题 * * */ public class Demo { public static void main(String[] args){ final StringBuffer s1 = new StringBuffer(); final StringBuffer s2 = new StringBuffer(); new Thread(){ @Override public void run() { //先拿锁一,再拿锁二 synchronized (s1){ s1.append("a"); s2.append("1"); synchronized (s2) { s1.append("b"); s2.append("2"); System.out.println(s1); System.out.println(s2); } } } }.start(); //使用匿名内部类实现runnable接口的方式实现线程的创建 new Thread(new Runnable() { @Override public void run() { synchronized (s2){ s1.append("c"); s2.append("3"); synchronized (s1) { s1.append("d"); s2.append("4"); System.out.println(s1); System.out.println(s2); } } } }).start(); } }
运行结果:
1.先调用上面的线程,再调用下面的线程:
2.出现死锁:
3.先调用下面的线程,再调用上面的线程。
死锁的解决办法:
1.减少同步共享变量
2.采用专门的算法,多个线程之间规定先后执行的顺序,规避死锁问题
3.减少锁的嵌套。线程的通信
通信常用方法:
通信方法 描述 wait() 一旦执行此方法,当前线程就进入阻塞状态,并释放同步监视器 notify 一旦执行此方法,就会唤醒被wait的一个线程,如果有多个线程,就唤醒优先级高的线程 notifyAll 一旦执行此方法,就会唤醒所有被wait()的线程 使用前提:这三个方法均只能使用在同步代码块或者同步方法中。
package com.example.paoduantui.Thread; /** * 线程通信的例子:使用两个线程打印1—100,线程1,线程2交替打印 * * 当我们不采取线程之间的通信时,无法达到线程1,2交替打印(cpu的控制权,是自动分配的) * 若想达到线程1,2交替打印,需要: * 1.当线程1获取锁以后,进入代码块里将number++(数字打印并增加)操作完以后,为了保证下个锁为线程2所有,需要将线程1阻塞(线程1你等等wait())。(输出1,number为2) * 2.当线程2获取锁以后,此时线程1已经不能进入同步代码块中了,所以,为了让线程1继续抢占下一把锁,需要让线程1的阻塞状态取消(通知线程1不用等了notify()及notifyAll()),即应该在进入同步代码块时取消线程1的阻塞。 * * */ class Number implements Runnable{ private int number = 1;//设置共享数据(线程之间对于共享数据的共享即为通信) //对共享数据进行操作的代码块,需要线程安全 @Override public synchronized void run() { while(true){ //使得线程交替等待以及通知交替解等待 notify();//省略了this.notify()关键字 if(number<100){ try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+":"+number); number++; try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } }else{ break; } } } } public class CommunicationTest { public static void main(String[] args){ //创建runnable对象 Number number = new Number(); //创建线程,并实现runnable接口 Thread t1 = new Thread(number); Thread t2 = new Thread(number); //给线程设置名字 t1.setName("线程1"); t2.setName("线程2"); //开启线程 t1.start(); t2.start(); } }
sleep和wait的异同:
相同点:一旦执行方法以后,都会使得当前的进程进入阻塞状态
不同点:
1.两个方法声明的位置不同,Thread类中声明sleep,Object类中声明wait。
2.调用的要求不同,sleep可以在任何需要的场景下调用,wait必须使用在同步代码块或者同步方法中
3.关于是否释放同步监视器,如果两个方法都使用在同步代码块或同步方法中,sleep不会释放,wait会释放经典例题:生产者/消费者问题:
生产者(Priductor)将产品交给店员(Clerk),而消费者(Customer)从店员处取走产品,店员一次只能持有固定数量的产品(比如20个),如果生产者视图生产更多的产品,店员会叫生产者停一下,如果店中有空位放产品了再通知生产者继续生产:如果店中没有产品了,店员会告诉消费者等一下,如果店中有产品了再通知消费者来取走产品。
这里可能出现两个问题:
生产者比消费者快的时候,消费者会漏掉一些数据没有收到。
消费者比生产者快时,消费者会去相同的数据。package com.example.paoduantui.Thread; /** * 线程通信的应用:生产者/消费者问题 * * 1.是否是多线程问题?是的,有生产者线程和消费者线程(多线程的创建,四种方式) * 2.多线程问题是否存在共享数据? 存在共享数据----产品(同步方法,同步代码块,lock锁) * 3.多线程是否存在线程安全问题? 存在----都对共享数据产品进行了操作。(三种方法) * 4.是否存在线程间的通信,是,如果生产多了到20时,需要通知停止生产(wait)。(线程之间的通信问题,需要wait,notify等) * * */ class Clerk{ private int productCount = 0; //生产产品 public synchronized void produceProduct() { if(productCount<20) { productCount++; System.out.println(Thread.currentThread().getName()+":开始生产第"+productCount+"个产品"); notify(); }else{ //当有20个时,等待wait try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } //消费产品 public synchronized void consumeProduct() { if (productCount>0){ System.out.println(Thread.currentThread().getName()+":开始消费第"+productCount+"个产品"); productCount--; notify(); }else{ //当0个时等待 try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } class Producer extends Thread{//生产者线程 private Clerk clerk; public Producer(Clerk clerk) { this.clerk = clerk; } @Override public void run() { try { sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()+";开始生产产品......"); while(true){ clerk.produceProduct(); } } } class Consumer implements Runnable{//消费者线程 private Clerk clerk; public Consumer(Clerk clerk) { this.clerk = clerk; } @Override public void run() { System.out.println(Thread.currentThread().getName()+":开始消费产品"); while(true){ try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } clerk.consumeProduct(); } } } public class ProductTest { public static void main(String[] args){ Clerk clerk = new Clerk(); Producer p1 = new Producer(clerk); p1.setName("生产者1"); Consumer c1 = new Consumer(clerk); Thread t1 = new Thread(c1); t1.setName("消费者1"); p1.start(); t1.start(); } }
-
Java多线程如何实现资源共享
2015-04-11 22:21:04Java实现多线程方式有两种:继承Thread类或者实现Runnable即可.线程启动时调用start()方法. 实现Runnable接口可以实现资源共享 下面让我们来看一下代码:public class Thread1 extends Thread{ private int num = ...Java实现多线程方式有两种:继承Thread类或者实现Runnable即可.线程启动时调用start()方法.
实现Runnable接口可以实现资源共享
下面让我们来看一下代码:public class Thread1 extends Thread{ private int num = 5; @Override public void run() { for(int i=0;i<10;i++) { if(this.num>0) { System.out.println("剩余的票1:"+num--); } } } } public class Thread2 implements Runnable{ private int num = 5; public void run() { for(int i=0;i<10;i++) { if(this.num>0){ System.out.println("剩余的票2:"+num--); } } } } /** * @author 付玉伟 * @time 2015-4-9 下午09:37:56 * @param args */ public static void main(String[] args) { Thread1 t1 = new Thread1(); t1.setName("售票窗口1"); Thread1 t2 = new Thread1(); t1.setName("售票窗口2"); Thread1 t3 = new Thread1(); t1.setName("售票窗口3"); t1.start(); t2.start(); t3.start(); Thread2 t1_ = new Thread2(); Thread t1_1 = new Thread(t1_); t1_1.setName("售票窗口1_"); Thread t2_2 = new Thread(t1_); t2_2.setName("售票窗口2_"); Thread t3_3 = new Thread(t1_); t1_1.setName("售票窗口3_"); t1_1.start(); t2_2.start(); t3_3.start(); } }
运行结果如下:
一共5张票,线程1卖了15次,显然资源没有共享,而线程2只买了5次。
Java多线程访问共享资源的方式:
1、如果每一个线程执行的代码相同,可以使用同一个runnable对象,这个对象中有那个共享数据(买票系统)
2、如果每一个线程执行的代码不相同,这时候需要不同的Runnable对象,有以下两种方式来实现这些Runnable对戏之间的数据共享。
(1)、将共享数据封装在另外一个对象中,然后将这个对象逐一传递给各个Runnable对象。每个线程对共享数据的操作方法也分配到那个对象身上去完成,这样容易实现针对该数据进行的各个操作的互斥和通信。
(2)、将这些Runnable对象作为某一个类中的内部类,共享数据作为这个外部类中的成员变量,每个线程对共享数据的操作方法也分配给外部类,以便实现对共享数据进行的各个操作的互斥和通信,作为内部类的各个Runnable对象调用外部类的这些方法。
(3)、上面两种方式的组合:将共享数据封装在另外一个对象中,每个线程对共享数据的操作方法也分配到那个对象身上去完成,对象作为这个外部类中的成员变量或方法中的局部变量,每个线程的Runnable对象作为外部类中的成员内部类或局部内部类。
(4)、总之,要同步互斥的几段代码最好是分别放在几个独立的方法中,这些方法再放在同一个类中,这样比较容易实现它们之间的同步互斥和通信。
3、极端且简单的方式,即在任意一个类中定义一个static的变量,这将被所有线程共享
在线程操作中由于其操作的不确定性,所以提供了一个方法,可以取得当前操作线程:
public static Thread currentThread();
说明:
对于线程的名字一般是在启动前进行设置,最好不要设置相同的名字,最好不要为一个线程改名字.
在Java执行中一个Java程序至少启动2个线程:一个主线程和一个垃圾回收线程.
多线程的同步问题
如果使用Runnable的方式实现买票系统,在买票出现延迟时如:public class Thread2 implements Runnable{ private int num = 5; public void run() { for(int i=0;i<10;i++) { try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } if(this.num>0){ System.out.println("剩余的票2:"+num--); } } } }
输出结果如下:
如果解决这样的问题就必须使用同步,即:过个操作在同一个时间段内只有一个线程进行。
Java多线程同该方法步主要依赖于若干方法和关键字
1、wait方法
Object的方法,作用是使得当前调用wait方法所在部分的线程停止执行,并释放当前所获得调用wait的代码块的锁,并在其他线程调用notify或者notifyAll方法时恢复到竞争锁状态
wait被调用的时候必须在拥有锁(synchronized修饰)的代码块中
恢复执行后,从wait的下一条语句开始执行,因为wait方法总是应当在while循环中调用,以免出现恢复执行后继续执行的条件却不满足继续执行的条件。
若wait方法参数中带时间,则除了notify和notifyAll被调用能激活处于wait状态的线程进入锁竞争外,在其他线程中interrupt它或者参数时间到了之后,该线程也将激活到竞争状态。
wait方法被调用的线程必须获得之前执行到wait时释放掉用的锁重新获得才能够恢复执行。
2、notify方法和notifyAll方法
notify方法通过调用了wait方法,但是尚未激活一个线程调度队列(即进入竞争锁),不是立即执行。并且具体是哪一个线程不能保证。另外一点就是被唤醒的这个线程一定是等待wait所释放的锁。
notifyAll方法则唤醒所有调用wait方法,尚未激活的进程进入竞争队列。
3、synchronized关键字
用来标识一个普通方法时,表示一个线程要执行该方法必须取得该方法所在对象的锁。
用来 标识一个静态方法时,表示一个线程要执行该方法必须取得该方法所在类的类锁
修饰一个代码块。类似synchronized(Obj){}表示一个线程要执行的代码块必须获得Object的锁,这样做的目的是减小锁的粒度,保证当不同块所需的锁不冲突时不用对整个对象加锁。利用零长度的byte数组对象做obj非常经济。
4、atomic action 原子操作
在Java中,以下都是原子操作,但是在C和C++中不是
对引用变量和除了long和double之外的原始数据类型变量进行读写
对所有声明为volatile的变量(包括long和double)的读写
另外在Java.util.concurrent和java.util.concurrent.atomic包中提供了一些不依赖于同步机制的线程安全类和方法。
下面使用synchronized来解决同步的问题:
1、同步代码块public class Thread2 implements Runnable{ private int num = 5; public void run() { for(int i=0;i<10;i++) { // 使用同步代码块 synchronized(this){ try { Thread.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); } } if(this.num>0){ System.out.println("剩余的票2:"+num--); } } } }
2、同步方法
public class Thread3 implements Runnable{ private int num = 5; public void run() { for(int i=0;i<10;i++){ sale(); } } // 使用同步方法 public synchronized void sale(){ try { Thread.sleep(300); //休息300毫秒 } catch (InterruptedException e) { e.printStackTrace(); } if(num > 0 ){ System.out.println(Thread.currentThread().getName()+"买票"+this.num--); } } public static void main(String[] args){ Thread3 t3 = new Thread3(); new Thread(t3,"售票窗口1").start(); new Thread(t3,"售票窗口2").start(); new Thread(t3,"售票窗口3").start(); } }
多线程之间资源共享需要使用同步,但是过多的同步会造成死锁。
死锁是在多道程序系统中,一组进程中的每一个进程都无限期的等待另一个线程,所以占有且永远不会释放的资源。
死锁产生的原因:
1、竞争资源,系统提供的资源有限,不能满足每一个进程的要求
2、多道程序运行时,进程推进顺序不合理
产生死锁的必要条件:
1、互斥资源使用
2、占用并等待资源
3、不可抢夺资源
4、循环等待资源
转自http://www.cnblogs.com/dennisit/archive/2013/02/24/2925288.html -
JAVA 继承Thread 实现多线程 资源不共享? 请保持清醒 。
2021-11-12 10:13:40继承Thread 实现多线程 , 是 ‘ 不 易 ’ 实现 资源共享 , 而不是 不能实现资源共享 !!! 看看现在网上的有些文章 (包括一些所谓的面试宝典,文字简短但是害人不浅),随便截一个图: 然后类似的例子 ... -
java多线程共享数据、线程同步与互斥
2020-01-21 15:34:18本文全文以售票系统为例,简诉了java多线程间共享数据的两种方式、线程同步。文章可能还有很多不足,请大家谅解,欢迎大佬提意见。 本文使用到的东西 java eclipse 2019-11 文章目录写在前面本文使用到的东西1.多... -
java多线程全局变量共享问题
2018-07-17 20:14:09先看下面问题:多个线程访问全局变量x,然后将x与i累加,启动10个线程,想让每个线程的输出结果都是一样的55,但是实际不是的。 package ThreadTest; public class Counter { private int x =0; // 计数方法... -
Java多线程 - 锁
2022-03-28 15:31:49Java多线程 - 锁 三性 可见性 指的是线程之间的可见性,一个线程对状态的修改,对其他线程是可见的。在 Java中 volatile、synchronized 和 final 实现可见性。 原子性 如果一个操作是不可分割的,我们则称之为... -
java 多线程 session共享
2015-09-22 09:00:35在struts2中,启动新的线程 调用ServletActionContext.getRequest() 报空指针错误 -
java 学习~多线程通信 使用共享变量 例子和解释
2022-03-07 21:32:04多线程互相通信一般使用共享变量。。 完整验证代码: main 方法 public class Test5 { public static void main(String[] args) { Datax m1=new Datax(); Threadx t1=new Threadx(m1) ; Thready t2= new ... -
Java 多线程(超详细)
2021-01-12 21:14:38多线程学习思路:为什么学习线程?为了解决CPU利用率问题,提高CPU利用率。 =》 什么是进程?什么是线程? =》 怎么创建线程?有哪几种方式?有什么特点? =》 分别怎么启动线程? =》 多线程带来了数据安全问题,该... -
Java:线程间数据共享
2018-11-09 23:51:07并发运行 线程中并发指一个时间段中多个线程都处于已启动但没有运行结束的状态。 多个线程之间默认并发运行,这种运行方式往往会出现交叉的情况。...使原本并发运行的多个...蓝框:多线程间共享的数据 红框... -
Java多线程面试题(面试必备)
2020-05-26 01:15:38文章目录一、多线程基础基础知识1. 并发编程1.1 并发编程的优缺点1.2 并发编程的三要素1.3 并发和并行有和区别1.4 什么是多线程,多线程的优劣?2. 线程与进程2.1 什么是线程与进程2.2 线程与进程的区别2.3 用户线程... -
JAVA多线程——共享变量
2018-11-12 10:53:09【转载】Java多线程编程:变量共享分析(Thread) 原博客网址:https://www.cnblogs.com/xudong-bupt/archive/2013/05/22/3087864.html 今天看到java的多线程,感到十分激动,之前就在python中使用过多... -
Java 多线程编程基础(详细)
2020-11-03 17:36:30Java多线程编程基础进程与线程多线程实现Thread类实现多线程Runnable接口实现多线程Callable接口实现多线程多线程运行状态多线程常用操作方法线程的命名和获取线程休眠线程中断线程强制执行线程让步线程优先级设定... -
Java多线程面试题,我丝毫不慌
2020-07-28 09:18:51} } 结果还是跟上面是一样的,这里我就不贴图了~~~ 1.6Java实现多线程需要注意的细节 不要将run()和start()搞混了~ run()和start()方法区别: run():仅仅是封装被线程执行的代码,直接调用是普通方法 start():首先... -
万字图解Java多线程
2020-09-06 14:45:07但是真正有多线程的需求却不知道怎么下手,实际上还是对多线程这块知识了解不深刻,不知道多线程api的应用场景,不知道多线程的运行流程等等,本篇文章将使用实例+图解+源码的方式来解析java多线程。 文章篇幅较长,... -
Java多线程系列—多线程带来的问题(05)
2021-05-05 14:08:35为了充分利用CPU资源,为了提高CPU的使用率,采用多线程的方式去同时完成几件事情而不互相干扰,为了处理大量的IO操作时或处理的情况需要花费大量的时间等等,比如:读写文件,视频图像的采集,处理,显示,保存等。... -
Java多线程(2万字深入理解多线程,有实例代码辅助理解)
2020-04-08 09:35:01线程概述 线程的创建 1. 继承Thread类 2. 实现Runnable接口 3. 实现Callable接口 4. 使用线程池 线程的调度 线程的生命周期 线程同步 1. 方法一:同步代码块 2. 方法二:同步方法 3. 方法三:Lock(锁): 4. 对比三种... -
JAVA多线程是什么
2020-08-31 23:53:23一、 什么是多线程: ...多线程:线程是进程内部比进程更小的执行单元(执行流|程序片段),每个线程完成一个任务,每个进程内部包含了多个线程每个线程做自己的事情,在进程中的所有线程共享该进程的资源; 主 -
Java多线程共享变量控制
2019-06-27 21:44:54如果一个变量同时在多个线程的工作内存中存在副本,那么这个变量就叫共享变量 2. JMM(java内存模型) 多个线程同时对主内存的一个共享变量进行读取和修改时,首先会读取这个变量到自己的工作内存中成为一个副本,... -
【JAVA核心知识】17.1:线程间变量共享
2021-06-15 15:06:40变量共享是进行线程间通信的常用手段之一,java的内存模型(JMM)解决了可见性和有序性,我们则可以通过线程锁保证原子性。 共享继承Runnable将变量作为成员变量将变量定义为final直接访问 有变量: class ShareData... -
java多线程详细理解
2021-02-14 21:10:51明确一点:多线程不是为了提高程序执行速度(性能甚至更低),而是提高应用程序的使用效率。 多线程的三大特性:原子性、可见性、有序性 一、创建线程 创建线程额的开销:分配内存 --> 列入调度 --> 线程切换... -
java多线程会造成线程安全问题的原因总结
2021-09-30 22:13:28众所周知,多线程会造成线程安全问题,那么多线程为什么会导致线程安全问题呢? 一:首先了解jvm内存的运行时数据区 1.堆区:存储对象实例(和实例变量),数组等 2.java虚拟机栈(方法·栈),存放方法声明,... -
Java多线程学习(吐血超详细总结)
2015-03-14 13:13:17本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。 -
Java多线程(四)——多线程数据隔离与共享
2019-02-26 15:33:16多线程数据隔离与数据共享是个矛盾体,有些数据需要隔离,比如每个人的银行账户,有些需要共享比如买票的总火车票数量,这个问题导致了这篇文章的出现,抽象出了这两类问题。 二、数据隔离 ... -
2021年JAVA多线程并发编程面试题(持续更新)
2021-02-18 12:56:25Java 中用到的线程调度算法是什么?线程同步以及线程调度相关的方法。sleep() 和 wait() 有什么区别?线程的 sleep()方法和 yield()方法有什么区别?同步方法和同步块,哪个是更好的选择?如果你提交任务时,线程池... -
【java并发】多个线程间共享数据
2016-05-31 22:56:36先看一个多线程间共享数据的问题: 设计四个线程,其中两个线程每次对data增加1,另外两个线程每次对data减少1。 从问题来看,很明显涉及到了线程间通数据的共享,四个线程共享一个data,共同操作一个data。我们... -
Java多线程(超详细!)
2021-05-12 17:00:59注意:一个进程可以启动多个线程。 eg.对于java程序来说,当在DOS命令窗口中输入: java HelloWorld 回车之后。 会先启动JVM,而JVM就是一个进程。 JVM再启动一个主线程调用main方法。 同时再启动一个垃圾回收线程...