Java并发编程并发基础 —— 线程
Posted 刘婉晴
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java并发编程并发基础 —— 线程相关的知识,希望对你有一定的参考价值。
一、线程
线程是操作系统调度的最小单元,多线程同时执行,可以提高程序性能 。
1. 什么是线程
操作系统运行一个程序,就会创建一个进程,在一个进程里可以创建多个线程,因此线程也叫做轻量级进程 。
2. 线程带来了什么好处
现代处理器都是多核的,程序运行过程中能够创建多个线程,而一个线程在一个时刻只能运行在一个处理器核心上,如果一个单线程程序在运行时只能使用一个处理器核心,那么再多的处理器核心加入也无法显著提升该程序的执行效率。
3. 线程基础
(1)优先级 —— setPriority(int)
线程优先级决定线程需要多或者少分配一些处理器资源的线程属性
设置线程优先级时,针对频繁阻塞(休眠或者I/O操作)的线程需要设置较高优先级,而偏重计算(需要较多CPU时间或者偏运算)的线程则设置较低的优先级,确保处理器不会被独占。
程序正确性不能依赖于优先级高低 —— 不同操作系统有不同的处理方式
(2)状态 ——
New 初始态\\ Running 运行态 \\ Blocked 阻塞态 \\Waiting 等待态\\ TimeWaiting 超时等待态\\ Terminated 终止态
注: Java将操作系统中的运行和就绪两个状态合并称为运行状态
阻塞状态是线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态,但是阻塞在java.concurrent包中Lock接口的线程状态却是等待状态,因为java.concurrent包中Lock接口对于阻塞的实现均使用了LockSupport类中的相关方法。
(3)Daemon 线程 —— setDaemon(true)
支持型线程,用作程序中台调度以及支持性工作
当一个Java虚拟机中不存在非Daemon线程的时候,Java虚拟机将会退出
(4)线程的启动和终止
新构造的线程对象是由其parent线程来进行空间分配的,child线程继承了parent是否为Daemon、优先级和加载资源的contextClassLoader以及可继承的ThreadLocal,同时还会分配一个唯一的ID来标识这个child线程,至此,一个能够运行的线程对象就初始化好了。
start()方法的含义是:当前线程(即parent线程)同步告知Java虚拟机,只要线程规划器空闲,应立即启动调用start()方法的线程
(5)中断 Thread.currentThread().isInterrupted()
- 中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作;
- 中断状态是线程的一个标识位,中断操作是一种简便的线程间交互方式,这种交互方式适合用来取消或停止任务
- 除了中断以外,还可以利用一个boolean变量来控制是否需要停止任务并终止该线程
4. volatile和synchronized关键字
volatile
告知程序任何对该变量的访问均需要从共享内存中获取,而对它的改变必须同步刷新回共享内存,它能保证所有线程对变量访问的可见性。
synchronized
关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它保证了线程对变量访问的可见性和排他性。
- 本质是对一个对象的监视器(monitor)进行获取,而这个获取过程是排他的,也就是同一时刻只能有一个线程获取到由synchronized所保护对象的监视器 ——
任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取到该对象的监视器才能进入同步块或者同步方法,而没有获取到监视器(执行该方法)的线程将会被阻塞在同步块和同步方法的入口处,进入BLOCKED状态。
5. 等待通知
等待/通知机制是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()或者notifyAll()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。
等待/通知的相关方法是任意Java对象都具备的,因为这些方法被定义在所有对象的超类java.lang.Object上。
- 使用wait()、notify()和notifyAll()时需要先对调用对象加锁;
- 调用wait()方法后,线程状态由RUNNING变为WAITING,并将当前线程放置到对象的等待队列;
- notify()或notifyAll()方法调用后,等待线程依旧不会从wait()返回,需要调用notify()或notifAll()的线程释放锁之后,等待线程才有机会从wait()返回;
- notify()方法将等待队列中的一个等待线程从等待队列中移到同步队列中,而notifyAll()方法则是将等待队列中所有的线程全部移到同步队列,被移动的线程状态由WAITING变为BLOCKED;
- 从wait()方法返回的前提是获得了调用对象的锁。
synchronized(对象)
while(条件不满足)
对象.wait();
对应的处理逻辑
synchronized(对象)
改变条件
对象.notifyAll();
(1)Thread.join()
线程A执行了thread.join()语句,其含义是:当前线程A等待thread线程终止之后才从thread.join()返回
join() 当线程终止时,会调用线程自身的notifyAll()方法,会通知所有等待在该线程对象上的线程。
(2)ThreadLocal
以ThreadLocal对象为键、任意对象为值的存储结构,一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。
ThreadLocal 调用耗时统计的功能上,在方法的入口前执行begin()方法,在方法调用后执行end()方法,好处是两个方法的调用不用在一个方法或者类中,比如在AOP(面向方面编程)中,可以在方法调用前的切入点执行begin()方法,而在方法调用后的切入点执行end()方法,这样依旧可以获得方法的执行耗时。
6. 如何实现等待超时模式
场景:调用一个方法时等待一段时间0,如果该方法能够在给定的时间段之内得到结果,那么将结果立刻返回,反之,超时返回默认结果。
解决:对象加锁 条件循环 处理逻辑 + 设置超时时间段
应用:超时等待模式可以用来实现 数据库连接池 中数据库连接操作超时返回
好处: 保证客户端线程不会一直挂在连接获取的操作上,而是“按时”返回,并告知客户端连接获取出现问题,是系统的一种自我保护机制,针对昂贵资源(比如数据库连接)的获取都应该加以超时限制。
7. 线程池
为什么需要考虑 C/S 架构的程序,如果没有线程池, 我们客户端向服务器发的请求,每个请求创建一个线程,假设我们有 1 万个请求, 那么就需要创建 1 万的线程进行处理。 众所周知,线程的创建和上下文切换是非常消耗资源的。因此, 需要线程池技术解决这个问题 。
线程池好处
- 消除了频繁创建和消亡线程的系统资源开销
- 面对过量任务的提交能够平缓的劣化
线程池的默认实现
使用一个线程安全的工作队列连接工作者线程和客户端线程。
线程池应用: 简单的 Web 服务器
常用的Java Web服务器,如Tomcat、Jetty,在其处理请求的过程中都使用到了线程池技术。 因为我们不能一个一个请求的顺序处理,这样的话,估计就没有用户会想用了 。现代的浏览器可以并发发送多个请求, 服务器可以并发处理多个请求 。
线程池中线程数量设置线程池中线程数量并不是越多越好,具体的数量需要评估每个任务的处理时间,以及当前计算机的处理器能力和数量。使用的线程过少,无法发挥处理器的性能;使用的线程过多,将会增加系统的无故开销,起到相反的作用。
Java并发编程基础(入门篇)
@
java中的多线程(入门)
1.线程
1.1多线程的执行原理
先以一个Java中多线程的Demo为例展开解释,请看代码:
//自定义的多线程类
public class MyThread extends Thread{
public MyThread(String name){
super(name);
}
//重写run方法
@Override
public void run(){
for(int i=0;i<20;i++){
System.out.println(getName()+"_"+i);
}
}
}
//测试类
public class ThreadTest {
public static void main(String[] args) {
//多线程的执行原理及其步骤:
//1.Jvm执行main方法,找OS开辟一条通向cpu的路径,这个
//路径就叫mian线程(主线程),cpu通过这个路径执行main中的方法
//新开辟一条通向cpu的新路径,这条新路径就是MyThread线程
MyThread mt=new MyThread("bob");
//使用MyThread线程执行run方法
mt.start();
for(int i=0;i<20;i++){
System.out.println("mian_"+i);
}
}
}
上述代码执行流程:
- 当jvm执行main方法时候,jvm向OS申请开辟一条通往cpu的可执行路径,这条路径叫main线程。(cpu通过这条路径来执行main的所有方法)
- 继续执行,MyThread mt=new MyThread("bob"),jvm向os申请开辟一条新的通往cpu的路径,该路径为MyThread的线程。
- mt.start();cpu执行MyThread线程。
- 由于cpu既可以通向main线程,又可以通向MyThread线程,我们程序无法控制cpu的选择,其本身具备线程的调度策略,故两个线程会出现交叉执行的随机现象。
- main线程和MyThread线程处于并发的状态,两者轮流使用cpu。
图解多线程执行内存情况
注意: mt.start()方法做了两件事情:为mt线程开辟了新的栈空间;将该线程的run方法压栈执行;
1.2多线程的创建方法
方法1: 继承Thread类,在子类中重写run()方法,start()方法开启新的线程;
public class MyThread extends Thread{
public MyThread(String name){
super(name);
}
@Override
public void run(){
for(int i=0;i<20;i++){
System.out.println(getName()+"_"+i);
}
}
}
public class ThreadTest {
public static void main(String[] args) {
MyThread mt=new MyThread("bob");
mt.start();
}
}
方法2: 实现Runnnable接口,将该子类作为参数传递给Thread的构造方法;使用Thread对象的start()方法启动新的线程;
public class MyRunnable implements Runnable{
@Override
public void run(){
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+"_"+i);
}
}
}
public class ThreadTest {
public static void main(String[] args) {
Thread th=new Thread(new MyRunnable());
th.start();
System.out.println("主线程");
}
}
1.3Thread和Runnable的区别
实现Runnable的好处:
- Runnable避免java中的单继承的局限性
- 增强了程序的扩展性,降低了程序的耦合性;把设置线程任务和开启线程任务实现了分离
- 多个线程可以共享同一个线程任务
- 线程池只能放入实现了Runnable或者Collable接口的类实例,不能放置继承Thread类的子类对象;
1.4匿名内部类方式实现线程的创建
public class ThreadTest {
public static void main(String[] args) {
//匿名内部类
//规则:new 父类/接口(){
//重写父类中的run方法;
//}
new Thread(new Runnable(){
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName());
}
}
}).start();
System.out.println("main");
}
}
2.线程的安全
多线程访问了共享数据会导致线程问题
卖票案例
public class Ticket implements Runnable {
private int tickets=100;
@Override
public void run(){
while(true){
if(tickets>0){
try{
Thread.sleep(100);
}catch(Exception e){
e.printStackTrace();
}
String name=Thread.currentThread().getName()+"正在卖票";
System.out.println(name+"正在卖票:"+tickets--);
}
}
}
}
public class SaleTickets {
public static void main(String[] args) {
Ticket ticket=new Ticket();
Thread t1=new Thread(ticket,"窗口1");
Thread t2 = new Thread(ticket, "窗口2");
Thread t3 = new Thread(ticket, "窗口3");
t1.start();
t2.start();
t3.start();
}
}
产生安全问题的原因
多个线程访问了同一个变量。
使用java的线程同步机制来解决线程安全问题
- synchronized同步代码块
- synchronized同步方法
- lock锁机制(可以手动加锁和释放锁)
//同步块
public class Ticket implements Runnable {
private int tickets=100;
Object obj=new Object();
@Override
public void run(){
while(true){
if(tickets>0){
synchronized(obj){
try{
Thread.sleep(100);
}catch(Exception e){
e.printStackTrace();
}
String name = Thread.currentThread().getName() + "正在卖票";
System.out.println(name + "正在卖票:" + tickets--);
}
}
}
}
}
//同步方法块
public class Ticket implements Runnable {
private int tickets=100;
Object obj=new Object();
@Override
public void run(){
saleTicket();
}
public synchronized void saleTicket(){
while (true) {
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
String name = Thread.currentThread().getName() + "正在卖票";
System.out.println(name + "正在卖票:" + tickets--);
}
}
}
}
//使用lock锁机制
public class Ticket implements Runnable {
private int tickets=100;
//创建锁对象
Lock lock=new ReentrantLock();
@Override
public void run(){
while (true) {
lock.lock();//加锁
if (tickets > 0) {
try {
Thread.sleep(100);
} catch (Exception e) {
e.printStackTrace();
}
String name = Thread.currentThread().getName() + "正在卖票";
System.out.println(name + "正在卖票:" + tickets--);
}
lock.unlock();//释放锁
}
}
}
同步原理
t0、t1、t2三个线程同时去抢夺cpu资源,假如t0获取到了cpu执行权限,t0遇到synchronized(object),获取对象锁,进入到同步代码块,当同步代码块执行完成之后,才归该对象锁。
3.线程的状态
- new(可运行,但没有运行.start())
- Runnable(正在运行的线程)
- blocked(没有获取到锁对象,有cpu执行权力)
- terminated(终止状态,run方法完成,异常终止)
- timed_waiting(计时等待,时间到了,自己恢复)
- waiting(永久等待,等待锁对象调用notify()唤醒)
生产者消费者案例
- 同步锁对象是唯一,并且是多个线程所共享的
- 使用锁对象的wait()和notify()来使得当前线程休眠和唤醒,两个方法都必须处于synchronized的代码块或方法中;
public class WaitAndNotify {
public static void main(String[] args) {
//保证生产者和消费者使用同一个对象锁
Object obj=new Object();
//消费者
new Thread(new Runnable(){
@Override
public void run() {
while(true){
synchronized (obj) {
System.out.println("我要吃包子");
try {
obj.wait();// 当前线程进入永久等待,并且规划对象锁,直到收到唤醒通知;
System.out.println("开始吃包子");
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}).start();
//生产者
new Thread(new Runnable(){
@Override
public void run() {
while(true){
// 唤醒消费者线程
synchronized (obj) {
try {
// 生产包子,1秒
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
obj.notify();//发出唤醒由该对象锁进入等待的线程,代码出了synchronized代码块后自动归还对象锁;
}
}
}
}).start();
}
}
使用了object.wait()之后,当前线程处于永久等待,并且归还该对象锁,直到收到object.notify()的唤醒通知。
并发编程
并发编程的硬件基础
现代CPU架构中,一个CPU通常可以包含多个物理内核,而一个内核又通过虚拟技术可以实现双线程,因此一个两核CPU可以同时运行4个线程。
什么是并发?
在指定的一段时间内,可同时运行多个线程,并发编程的目的在于确定每个线程任务分配的cpu及其相应的开始和结束时间以使得cpu的利用率最高,同时必须保证线程的安全!
并发编程的三大特性
原子性
即定义的一系列操作要么全部执行成功,要么一个都不执行。
可见性
多个线程共享同一个变量的时候,当一个线程修改了该变量的时候,其他线程必须能够立刻获取到修改后的值。
有序性
程序执行的顺序按照代码的先后顺序执行;java编译器为了提高程序效率,会对代码的指令进行重排,在单线程程序中,会保证程序最后的执行结果和重排序之前一致,但是在多线程程序中,有可能会影响并发的结果。因此,在并发编程中,必须保证有序性。
java如何保证并发编程的三大特性
Java的内存模型
Java内存模型规定,所有的变量均存储在主存(物理内存),每一个线程都拥有自己的工作内存,也叫缓存(主存中的副本,线程不直接修改主存中的变量,每个线程不能访问其他线程的工作内存)cpu中还有高速缓存行(l1,l2,l3寄存器),线程的工作内存数据先被读取到cpu缓存器中,因此数据存在三个级别的。举个例子:
i=250;
执行该行代码的时候,先从主存中取i的值到当前线程的工作内存中,然后将250赋值给工作内存中的i变量,最后,将工作内存中的i变量写入到主存中去。
原子性
java中对单个的读或者单个的写操作,是具有原子性的,即要么成功,要么失败。但是对任何单一的操作的组合而成的复杂操作是无法保证原子性的。举个例子:
i++;
i++自增操作不具备原子性的,为什么?i++操作其实是由三个操作构成:1)读取i的值。2)i的值增1. 3)将新值赋值给i变量;java语言只能保证单一的操作具有原子性。而要保证诸如i++等的原子性,可以使用synchronized和lock锁机来同步这些代码块,可以保证并发编程的原子性。
可见性
那java中如何保证变量的可见性呢?volatile关键字。当被volatile修饰的变量(当前线程的共内存中)被修改时,能被立即刷新到主存中去。因此,可以保证在其他线程还没有从主存中读去新值的情况下,可以得到最新的值。 ,同时 synchronized 和 lock能保证在同一个时刻只有一个线程能获取到对象锁(对象监视器)然后执行同步代码块,并且在释放对象锁之前将修改的值刷新到主存中去,从而在一定程度上保证了变量的可见性。
有序性
Java内存模型中,允许编译器和处理器对指令进行重排,指令重排不会影响共到单线程,但是会影响到多线程并发执行的正确性。java中的"happens-before"原则能保证一定的有序性。"happen-before"有8大原则:
- 程序次序规则。在一个程序内,书写在前面的操作先于后面的操作
- 锁定规则。同一锁对象的Lock操作先于Unlock操作
- volatile变量规则。线程1对该变量的写操作如果先于线程2对该变量的读操作,那么线程2的读操作才能读取到线程1写入的值。
- 传递规则。如果A先于B,B先于C ,那么A一定先于C
- 线程启动规则。Thread对象的start()方法先于此线程的任意动作
- 线程中断规则。对线程的interupt()操作一定先于该线程中断事件发生和检测。
- 线程终止规则。线程的终止检测是该线程中最后的操作,因此可以使用Thread.join()来终止线程和Thread.isAlive()来检测线程是否终止
- 对象终结规则。一个对象的初始化操作先于对象的finalize()操作的开始。
volatile关键字的详细解释
一个共享变量如果被volatile所修饰,那么该变量就具备以下两种含义:
- 该变量对于所有线程具有可见性
- 该变量及其相关变量(有依赖关系)不允许指令重排,从而保证了有序性。
举个例子,以下代码线程1先执行。
//线程1
boolean stop = false;
while(!stop){
doSomething();
}
//线程2
stop = true;
分析: 线程1中执行以下操作,先将主存中的stop变量读取到线程1的工作内存中,线程1进入死循环,一直执行doSomething()操作。而当线程2开始执行的时候,情况变了,线程2先将主存中的stop变量读取到工作内存中,此时有可能存在两种情况:1)线程2修改了工作内存中的stop变量,但是没有写入到主存中去就被终止了。 2)线程2修改了工作内存中的stop变量,并写入到主存中去。当情况1)发生时,那么线程1进入死循环。当情况2)发生时,线程2可以退出死循环。那么如何才能保证上述代码只正确执行呢??使用volatile修饰stop保证这个变量的可见性,即当这个变量在某个线程的工作内存中修改是就立即被刷新到主存中,其他线程工作内存中的缓存状态失效,并重新从主存中读取新值。
volatile能保证原子性吗
先上个案例,
public class Test {
public volatile int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) {
//保证前面的线程都执行完
Thread.yield();
}
System.out.println(test.inc);
}
}
分析: 代码创建了10个线程,每个线程使得inc自增1000,同时使用volatile修饰了inc变量,那inc最终的结果是10*1000吗??答:不是。为什么呢?因为虽然inc具有可见性,但是inc++操作不具备原子性,我们可以设想到这样的一个场景,某个状态下,inc=10,线程1从主存中读取了inc的值到工作内存中,inc还未进行自增的时候,线程1的cpu执行权限到了,转而去执行线程2,此时,线程2从主存中将inc读取到工作内存中,再将Inc读取到cpu的高速缓存寄存器中,进行自增操作inc+1=11,并写回主存,线程1已经读取了inc到工作内存中并且读取到cpu的l1、l2中去了,即使缓存中的数据更新过来,cpu用的仍然是更新之前的数据,因此也不会重新读取缓存中的值随后刷新到主存中,因此两个线程inc增加了1。
改进
使用synchronized或则lock机制来保证inc操作具有原子性。就可以让线程1准确地保证可见性,从使得并发能正确进行
public class Test {
public volatile int inc = 0;
Lock lock = new ReentrantLock();
//使用syunchronized保证原子性
public synchronized void increase() {
inc++;
}
//使用lock同步代码块,保证原子性
public void increase2(){
try{
lock.lock();
inc++;
lock.unlock();
}catch(Exception e){
..
}
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) {
//保证前面的线程都执行完
Thread.yield();
}
System.out.println(test.inc);
}
}
结论: volatile 能保证共享变量的可见性,但是无法保证对该变量的操作的原子性,保证原子性可以通过synchronized或者lock来保证。
volatile的作用
- 缓存一致性协议。cpu的硬件协议,每个cpu通过嗅探总线上传输的数据来检测自己的缓存中的数据是否失效,当cpu‘发现自己的缓存行(l1,l2,l3等寄存器,存放的是缓存的地址)中对应的内存中地址被修改的时候,就会将当前cpu的缓存设置为无效状态,当cpu对这个数据进行操作的时候,就会重新从主存中读取数据到缓存中。通过这种方式能保证可见性
- 禁止指令重排。假如一个变量被volatile修饰,那么与这个变量有依赖关系的前面所有变量的操作顺序只能先于该变量,同理其后的所有变量的操作在它之后执行,不允许被重排
线程内的共享变量(ThreadLocal)
ThreadLocal对象用于某个线程内的共享变量,ThreadLocal对象提供两个方法:set()和get()方法,两个方法实现底层为一个Map,Map的键为当前线程的ThreadLocal对象。因此不同的线程使用get()能拿到属于自己线程内的ThreadLocal对象,也就能保证拿到其中的值;同时,在同一个线程中能保证一个ThreadLocal对象只存储一个值,实现了线程内的共享。看个例子吧:
//Runnable的子类
public class MyThreadLocal implements Runnable {
//private Student student=new Student();
ThreadLocal<Student> tl=new ThreadLocal<Student>();
@Override
public void run(){
String currentThread=Thread.currentThread().getName();
System.out.println(currentThread+"正在运行");
System.out.println("开始设置对象的属性");
Random random=new Random();
Student student = getStudent();
student.setAge(random.nextInt(100));
System.out.println(currentThread+"线程中,第一次的年龄为:"+student.getAge()+",哈希值为:"+student.hashCode());
try {
Thread.sleep(1000);//让其他线程运行;
} catch (Exception e) {
e.printStackTrace();
}
student.setAge(random.nextInt(100));
System.out.println(currentThread + "线程中,第二次的年龄为:" + student.getAge() + ",哈希值为:" + student.hashCode());
}
public Student getStudent(){
Student s=tl.get();
if(null==s){
s=new Student();
tl.set(s);
}
return s;
}
}
//测试类
public class testThreadLocal {
public static void main(String[] args) {
MyThreadLocal threadLocal=new MyThreadLocal();
new Thread(threadLocal,"线程1").start();
new Thread(threadLocal,"线程2").start();
}
}
分析:在创建要线程内共享的变量之前,先从ThreadLocal中get()一份,如果有就不创建,没有才创建,并且使用set()方法将刚刚创建的对象存到ThreadLocalMap中去。
多个线程之间的数据共享
java传统的对线程间的数据共享通常有两种形式:
- 线程行为一致。多个线程使用同一个Runnable对象,即执行相同的代码段,共享数据在Runnable对象中。
- 线程行为不一致。多个线程的行为不一致,需要使用不同的Runnable对象,执行不同的代码段。
线程行为一致的代码可以参考上述的卖票系统,现在举例说明线程不一致的数据共享情况:
方法1 将共享数据封装,然后作为参数传递至Runnable对象
public class testShareData {
public static void main(String[] args) {
ShareData shareData=new ShareData(10);
for(int i=0;i<4;i++){
if(i%2==0){
new Thread(new IncRunnable(shareData), "Thread_" + i).start();
}else{
new Thread(new decRunnable(shareData), "Thread_" + i).start();
}
}
}
}
// 共享数据的自增行为的Runnable类
class IncRunnable implements Runnable {
private ShareData shareData;
public IncRunnable(ShareData shareData) {
this.shareData = shareData;
}
private void incShareData() {
shareData.inc();
}
@Override
public void run() {
incShareData();
}
}
// 共享数据的自减行为的Runnable类
class decRunnable implements Runnable {
private ShareData shareData;
public decRunnable(ShareData shareData) {
this.shareData = shareData;
}
private void decShareData() {
shareData.dec();
}
@Override
public void run() {
decShareData();
}
}
public class ShareData {
// 共享数据封装类
private int num;
public ShareData(int num) {
this.num = num;
}
// 数据自增
public synchronized void inc() {
num++;
System.out.println(Thread.currentThread().getName()+"num值为:"+num);
}
// 数据自减
public synchronized void dec() {
num--;
System.out.println(Thread.currentThread().getName() + "num值为:" + num);
}
}
方法2 使用匿名内部类的方式创建Runnable对象,可以减少参数传递的步骤。
public class testShareData {
public static void main(String[] args) {
//确保shareData这个引用只会指向一个new ShareData()对象
final ShareData shareData=new ShareData(10);
for(int i=0;i<4;i++){
if(i%2==0){
new Thread(new IncRunnable(){
@Override
public void run(){
shareData.inc();
}
}, "Thread_" + i).start();
}else{
new Thread(new decRunnable(){
@Override
public void run(){
shareData.dec();
}
}, "Thread_" + i).start();
}
}
}
}
public class ShareData {
// 共享数据封装类
private int num;
public ShareData(int num) {
this.num = num;
}
// 数据自增
public synchronized void inc() {
num++;
System.out.println(Thread.currentThread().getName()+"num值为:"+num);
}
// 数据自减
public synchronized void dec() {
num--;
System.out.println(Thread.currentThread().getName() + "num值为:" + num);
}
}
总结面向对像的特性之一:封装;
以上是关于Java并发编程并发基础 —— 线程的主要内容,如果未能解决你的问题,请参考以下文章
Java 并发编程 -- 并发编程线程基础(线程安全问题可见性问题synchronized / volatile 关键字CASUnsafe指令重排序伪共享Java锁的概述)
Java 并发编程 -- 并发编程线程基础(线程安全问题可见性问题synchronized / volatile 关键字CASUnsafe指令重排序伪共享Java锁的概述)