并发编程解惑之线程

并发编程解惑之线程,第1张

主要内容:

进程是资源分配的最小单位,每个进程都有独立的代码和数据空间,一个进程包含 1 到 n 个线程。线程是 CPU 调度的最小单位,每个线程有独立的运行栈和程序计数器,线程切换开销小。

Java 程序总是从主类的 main 方法开始执行,main 方法就是 Java 程序默认的主线程,而在 main 方法中再创建的线程就是其他线程。在 Java 中,每次程序启动至少启动 2 个线程。一个是 main 线程,一个是垃圾收集线程。每次使用 Java 命令启动一个 Java 程序,就相当于启动一个 JVM 实例,而每个 JVM 实例就是在 *** 作系统中启动的一个进程。

多线程可以通过继承或实现接口的方式创建。

Thread 类是 JDK 中定义的用于控制线程对象的类,该类中封装了线程执行体 run() 方法。需要强调的一点是,线程执行先后与创建顺序无关。

通过 Runnable 方式创建线程相比通过继承 Thread 类创建线程的优势是避免了单继承的局限性。若一个 boy 类继承了 person 类,boy 类就无法通过继承 Thread 类的方式来实现多线程。

使用 Runnable 接口创建线程的过程:先是创建对象实例 MyRunnable,然后将对象 My Runnable 作为 Thread 构造方法的入参,来构造出线程。对于 new Thread(Runnable target) 创建的使用同一入参目标对象的线程,可以共享该入参目标对象 MyRunnable 的成员变量和方法,但 run() 方法中的局部变量相互独立,互不干扰。

上面代码是 new 了三个不同的 My Runnable 对象,如果只想使用同一个对象,可以只 new 一个 MyRunnable 对象给三个 new Thread 使用。

实现 Runnable 接口比继承 Thread 类所具有的优势:

线程有新建、可运行、阻塞、等待、定时等待、死亡 6 种状态。一个具有生命的线程,总是处于这 6 种状态之一。 每个线程可以独立于其他线程运行,也可和其他线程协同运行。线程被创建后,调用 start() 方法启动线程,该线程便从新建态进入就绪状态。

NEW 状态(新建状态) 实例化一个线程之后,并且这个线程没有开始执行,这个时候的状态就是 NEW 状态:

RUNNABLE 状态(就绪状态):

阻塞状态有 3 种:

如果一个线程调用了一个对象的 wait 方法, 那么这个线程就会处于等待状态(waiting 状态)直到另外一个线程调用这个对象的 notify 或者 notifyAll 方法后才会解除这个状态。

run() 里的代码执行完毕后,线程进入终结状态(TERMINATED 状态)。

线程状态有 6 种:新建、可运行、阻塞、等待、定时等待、死亡。

我们看下 join 方法的使用:

运行结果:

我们来看下 yield 方法的使用:

运行结果:

线程与线程之间是无法直接通信的,A 线程无法直接通知 B 线程,Java 中线程之间交换信息是通过共享的内存来实现的,控制共享资源的读写的访问,使得多个线程轮流执行对共享数据的 *** 作,线程之间通信是通过对共享资源上锁或释放锁来实现的。线程排队轮流执行共享资源,这称为线程的同步。

Java 提供了很多同步 *** 作(也就是线程间的通信方式),同步可使用 synchronized 关键字、Object 类的 wait/notifyAll 方法、ReentrantLock 锁、无锁同步 CAS 等方式来实现。

ReentrantLock 是 JDK 内置的一个锁对象,用于线程同步(线程通信),需要用户手动释放锁。

运行结果:

这表明同一时间段只能有 1 个线程执行 work 方法,因为 work 方法里的代码需要获取到锁才能执行,这就实现了多个线程间的通信,线程 0 获取锁,先执行,线程 1 等待,线程 0 释放锁,线程 1 继续执行。

synchronized 是一种语法级别的同步方式,称为内置锁。该锁会在代码执行完毕后由 JVM 释放。

输出结果跟 ReentrantLock 一样。

Java 中的 Object 类默认是所有类的父类,该类拥有 wait、 notify、notifyAll 方法,其他对象会自动继承 Object 类,可调用 Object 类的这些方法实现线程间的通信。

除了可以通过锁的方式来实现通信,还可通过无锁的方式来实现,无锁同 CAS(Compare-and-Swap,比较和交换)的实现,需要有 3 个 *** 作数:内存地址 V,旧的预期值 A,即将要更新的目标值 B,当且仅当内存地址 V 的值与预期值 A 相等时,将内存地址 V 的值修改为目标值 B,否则就什么都不做。

我们通过计算器的案例来演示无锁同步 CAS 的实现方式,非线程安全的计数方式如下:

线程安全的计数方式如下:

运行结果:

线程安全累加的结果才是正确的,非线程安全会出现少计算值的情况。JDK 15 开始,并发包里提供了原子 *** 作的类,AtomicBoolean 用原子方式更新的 boolean 值,AtomicInteger 用原子方式更新 int 值,AtomicLong 用原子方式更新 long 值。 AtomicInteger 和 AtomicLong 还提供了用原子方式将当前值自增 1 或自减 1 的方法,在多线程程序中,诸如 ++i 或 i++ 等运算不具有原子性,是不安全的线程 *** 作之一。 通常我们使用 synchronized 将该 *** 作变成一个原子 *** 作,但 JVM 为此种 *** 作提供了原子 *** 作的同步类 Atomic,使用 AtomicInteger 做自增运算的性能是 ReentantLock 的好几倍。

上面我们都是使用底层的方式实现线程间的通信的,但在实际的开发中,我们应该尽量远离底层结构,使用封装好的 API,例如 JUC 包(javautilconcurrent,又称并发包)下的工具类 CountDownLath、CyclicBarrier、Semaphore,来实现线程通信,协调线程执行。

CountDownLatch 能够实现线程之间的等待,CountDownLatch 用于某一个线程等待若干个其他线程执行完任务之后,它才开始执行。

CountDownLatch 类只提供了一个构造器:

CountDownLatch 类中常用的 3 个方法:

运行结果:

CyclicBarrier 字面意思循环栅栏,通过它可以让一组线程等待至某个状态之后再全部同时执行。当所有等待线程都被释放以后,CyclicBarrier 可以被重复使用,所以有循环之意。

相比 CountDownLatch,CyclicBarrier 可以被循环使用,而且如果遇到线程中断等情况时,可以利用 reset() 方法,重置计数器,CyclicBarrier 会比 CountDownLatch 更加灵活。

CyclicBarrier 提供 2 个构造器:

上面的方法中,参数 parties 指让多少个线程或者任务等待至 barrier 状态;参数 barrierAction 为当这些线程都达到 barrier 状态时会执行的内容。

CyclicBarrier 中最重要的方法 await 方法,它有 2 个重载版本。下面方法用来挂起当前线程,直至所有线程都到达 barrier 状态再同时执行后续任务。

而下面的方法则是让这些线程等待至一定的时间,如果还有线程没有到达 barrier 状态就直接让到达 barrier 的线程执行任务。

运行结果:

CyclicBarrier 用于一组线程互相等待至某个状态,然后这一组线程再同时执行,CountDownLatch 是不能重用的,而 CyclicBarrier 可以重用。

Semaphore 类是一个计数信号量,它可以设定一个阈值,多个线程竞争获取许可信号,执行完任务后归还,超过阈值后,线程申请许可信号时将会被阻塞。Semaphore 可以用来 构建对象池,资源池,比如数据库连接池。

假如在服务器上运行着若干个客户端请求的线程。这些线程需要连接到同一数据库,但任一时刻只能获得一定数目的数据库连接。要怎样才能够有效地将这些固定数目的数据库连接分配给大量的线程呢?

给方法加同步锁,保证同一时刻只能有一个线程去调用此方法,其他所有线程排队等待,但若有 10 个数据库连接,也只有一个能被使用,效率太低。另外一种方法,使用信号量,让信号量许可与数据库可用连接数为相同数量,10 个数据库连接都能被使用,大大提高性能。

上面三个工具类是 JUC 包的核心类,JUC 包的全景图就比较复杂了:

JUC 包(javautilconcurrent)中的高层类(Lock、同步器、阻塞队列、Executor、并发容器)依赖基础类(AQS、非阻塞数据结构、原子变量类),而基础类是通过 CAS 和 volatile 来实现的。我们尽量使用顶层的类,避免使用基础类 CAS 和 volatile 来协调线程的执行。JUC 包其他的内容,在其他的篇章会有相应的讲解。

Future 是一种异步执行的设计模式,类似 ajax 异步请求,不需要同步等待返回结果,可继续执行代码。使 Runnable(无返回值不支持上报异常)或 Callable(有返回值支持上报异常)均可开启线程执行任务。但是如果需要异步获取线程的返回结果,就需要通过 Future 来实现了。

Future 是位于 javautilconcurrent 包下的一个接口,Future 接口封装了取消任务,获取任务结果的方法。

在 Java 中,一般是通过继承 Thread 类或者实现 Runnable 接口来创建多线程, Runnable 接口不能返回结果,JDK 15 之后,Java 提供了 Callable 接口来封装子任务,Callable 接口可以获取返回结果。我们使用线程池提交 Callable 接口任务,将返回 Future 接口添加进 ArrayList 数组,最后遍历 FutureList,实现异步获取返回值。

运行结果:

上面就是异步线程执行的调用过程,实际开发中用得更多的是使用现成的异步框架来实现异步编程,如 RxJava,有兴趣的可以继续去了解,通常异步框架都是结合远程 >

第一步,在创建好的数据库book,选中数据库book新建表。

第二步,添加数据库表字段id、ano、aname、asex、aage。

第三步,关闭插入字段窗口,这时会提示是否保存提示。

第四步,点击”是“按钮,输入表名称t_author_info。

第五步,刷新表,鼠标右键点击”设计“,打开表设计窗口,插入字段。

第六步,打开查询SQL窗口,编辑插入字段SQL语句。

SQL是1986年10月由美国国家标准局(ANSI)通过的数据库语言美国标准,接着,国际标准化组织(ISO)颁布了SQL正式国际标准。1989年4月,ISO提出了具有完整性特征的SQL89标准,1992年11月又公布了SQL92标准,在此标准中,把数据库分为三个级别:基本集、标准集和完全集。

1在新建的Project中右键新建Floder

2创建名为lib的包

3创建完毕之后的工程目录

4接下来解压你下载的mysql的jar包,拷贝其中的jar文件

5在工程lib包下邮件 选择paste即粘贴,把mysql的jar包拷贝进来

6拷贝完毕如图:

7在mysql的jar包上右键选择 build path - add to build path

8添加完毕之后,工程才与Mysql的jar包关联起来,现在可以使用相关类和方法了

9在工程中新建JdbcTest1java类

10输入如下代码:

11代码解释:

Driver是个实现类,它由具体的数据库厂商来实现。

它的connect方法可以获取数据库连接。参数如上图。

运行之后,输出如下,证明数据库连接成功!

12说明:这个是使用Driver连接数据库的,而通常开发中使用的是DriverManager或数据库连接池,这个仅作为理解数据库连接事例使用。

插入加密数据:

1、INSERT INTO userdata(username,pasword,encryptedpassword)

2、VALUES ('smith','htims',AES_ENCRYPT('htims','key'))

上面的插入语句有三个字段,“用户名”、“密码”和“加密的密码”。

AES_ENCRYPT()函数需要一个“key”来协助加密,同样,解密也需要它。

从表中查询加密数据

1、SELECT username,pasword,AES_DECRYPT(encryptedpassword,'key')

2、FROM userdata

spring中管理的bean实例默认情况下是单例的[sigleton类型],就还有prototype类型

按其作用域来讲有sigleton,prototype,request,session,global session。

spring中的单例与设计模式里面的单例略有不同,设计模式的单例是在整个应用中只有一个实例,而spring中的单例是在一个IoC容器中就只有一个实例。

但spring中的单例也不会影响应用的并发访问,不会出现各个线程之间的等待问题,或是死锁问题因为大多数时候客户端都在访问我们应用中的业务对象,而这些业务对象并

没有做线程的并发限制,只是在这个时候我们不应该在业务对象中设置那些容易造成出错的成员变量,在并发访问时候这些成员变量将会是并发线程中的共享对象,那么这个时候

就会出现意外情况。

那么我们的Eic-server的所有的业务对象中的成员变量如,在Dao中的xxxDao,或controller中的xxxService,都会被多个线程共享,那么这些对象不会出现同步问题吗,比如会造

成数据库的插入,更新异常?

还有我们的实体bean,从客户端传递到后台的controller-->service-->Dao,这一个流程中,他们这些对象都是单例的,那么这些单例的对象在处理我们的传递到后台的实体bean不

会出问题吗?

答:[实体bean不是单例的],并没有交给spring来管理,每次我们都手动的New出来的如EMakeType et = new EMakeType();,所以即使是那些处理我们提交数据的业务处理类

是被多线程共享的,但是他们处理的数据并不是共享的,数据时每一个线程都有自己的一份,所以在数据这个方面是不会出现线程同步方面的问题的。但是那些的在Dao中的

xxxDao,或controller中的xxxService,这些对象都是单例那么就会出现线程同步的问题。但是话又说回来了,这些对象虽然会被多个进程并发访问,可我们访问的是他们里面的方

法,这些类里面通常不会含有成员变量,那个Dao里面的ibatisDao是框架里面封装好的,已经被测试,不会出现线程同步问题了。所以出问题的地方就是我们自己系统里面的业务

对象,所以我们一定要注意这些业务对象里面千万不能要独立成员变量,否则会出错。

所以我们在应用中的业务对象如下例子;

controller中的成员变量List和paperService:

public class TestPaperController extends BaseController {

private static final int List = 0;

@Autowired

@Qualifier("papersService")

private TestPaperService papersService ;

public Page queryPaper(int pageSize, int page,TestPaper paper) throws EicException{

RowSelection localRowSelection = getRowSelection(pageSize, page);

List<TestPaper> paperList = papersServicequeryPaper(paper,localRowSelection);

Page localPage = new Page(page, localRowSelectiongetTotalRows(),

paperList);

return localPage;

}

service里面的成员变量ibatisEntityDao:

@SuppressWarnings("unchecked")

@Service("papersService")

@Transactional(rollbackFor = { Exceptionclass })

public class TestPaperServiceImpl implements TestPaperService {

@Autowired

@Qualifier("ibatisEntityDao")

private IbatisEntityDao ibatisEntityDao;

private static final String NAMESPACE_TESTPAPER = "comitsexamtestpapermodelTestPaper";

private static final String BO_NAME[] = { "试卷仓库" };

private static final String BO_NAME2[] = { "试卷配置试题" };

private static final String BO_NAME1[] = { "试卷试题类型" };

private static final String NAMESPACE_TESTQUESTION="comitsexamtestpapermodelTestQuestion";

public List<TestPaper> queryPaper(TestPaper paper,RowSelection paramRowSelection) throws EicException{

try {

return (List<TestPaper>) ibatisEntityDaoqueryForListWithPage(

NAMESPACE_TESTPAPER, "queryPaper", paper,paramRowSelection);

} catch (Exception exception) {

exceptionprintStackTrace();

throw new EicException(exception, "eic", "0001", BO_NAME);

}

}

由上面可以看出,虽然我们这个应用里面含有成员变量,但是并不会出现线程同步方面的问题,因为,controller里面的成员变量private TestPaperService papersService ;之

所以会成为成员变量,我们的目的是注入,将其实例化进而访问里面的方法,private static final int List = 0;是final的不会被改变。

service里面的private IbatisEntityDao ibatisEntityDao;是框架本身的线程同步问题已解决其解决方案很有可能就是使用ThreadLocal,见下面。

这下面的bean 一个是通过BeanFactory getBean得到,一个是业务对象testPapergetClass(),得到,通过不同客户端的浏览器访问,可得到下面结论,

springIoC容器管理的bean就是单例,因为不同的访问均得到相同的对象在应用开启的状态下,不重新启动应用下,即在同一次的应用运行中

-------------------------spring 中的sigleton ,这才是真正的整个应用下面就一个实例:class

comitsexamtestpaperserviceimplTestPaperServiceImpl$$EnhancerByCGLIB$$584b889d

-------------------------spring 中的sigleton ,这才是真正的整个应用下面就一个实例:class

comitsexamtestpaperserviceimplTestPaperServiceImpl$$EnhancerByCGLIB$$584b889d

-------------------------spring 中的sigleton ,这才是真正的整个应用下面就一个实例:class

comitsexamtestpaperserviceimplTestPaperServiceImpl$$EnhancerByCGLIB$$584b889d

-------------------------spring 中的sigleton ,这才是真正的整个应用下面就一个实例:class

comitsexamtestpaperserviceimplTestPaperServiceImpl$$EnhancerByCGLIB$$584b889d

Spring框架对单例的支持是采用单例注册表的方式进行实现的,详见“Spring设计模式——单例模式”这篇文章。

于spring如何实现那些个有状态bean[如RequestContextHolder、

TransactionSynchronizationManager、LocaleContextHolder]的线程安全,如下原理:详见

“ThreadLocal百度百科”,还可以参考网上这篇文章:“浅谈Spring声明式事务管理ThreadLocal和JDKProxy”。

虽然代码清单9‑3这个ThreadLocal实现版本显得比较幼稚,但它和JDK所提供的ThreadLocal类在实现思路上是相近的。

Java的多线程编程中,为保证多个线程对共享变量的安全访问,通常会使用synchronized来保证同一时刻只有一个线程对共享变量进行 *** 作。但在

有些情况下,synchronized不能保证多线程对共享变量的正确读写。例如类有一个类变量,该类变量会被多个类方法读写,当多线程 *** 作该类的实例对

象时,如果线程对类变量有读取、写入 *** 作就会发生类变量读写错误,即便是在类方法前加上synchronized也无效,因为同一个线程在两次调用方法之

间时锁是被释放的,这时其它线程可以访问对象的类方法,读取或修改类变量。这种情况下可以将类变量放到ThreadLocal类型的对象中,使变量在每个

线程中都有独立拷贝,不会出现一个线程读取变量时而被另一个线程修改的现象。

为了说明多线程访问对于类变量和

ThreadLocal变量的影响,QuerySvc中分别设置了类变量sql和ThreadLocal变量,使用时先创建

QuerySvc的一个实例对象,然后产生多个线程,分别设置不同的sql实例对象,然后再调用execute方法,读取sql的值,看是否是set方法

中写入的值。这种场景类似web应用中多个请求线程携带不同查询条件对一个servlet实例的访问,然后servlet调用业务对象,并传入不同查询条

件,最后要保证每个请求得到的结果是对应的查询条件的结果。

先创建一个QuerySvc实例对象,然后创建若干线程来调用

QuerySvc的set和execute方法,每个线程传入的sql都不一样,从运行结果可以看出sql变量中值不能保证在execute中值和set

设置的值一样,在

web应用中就表现为一个用户查询的结果不是自己的查询条件返回的结果,而是另一个用户查询条件的结果;而ThreadLocal中的值总是和set中设

置的值一样,这样通过使用ThreadLocal获得了线程安全性。

如果一个对象要被多个线程访问,而该对象存在类变量被不同类方法读写,为获得线程安全,可以用ThreadLocal来替代类变量。

同步机制的比较ThreadLocal和线程同步机制相比有什么优势呢?ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。

在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。

 

 而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对

数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多

线程代码时,可以把不安全的变量封装进ThreadLocal。

由于ThreadLocal中可以持有任何类型的对象,低版本

JDK所提供的get()返回的是Object对象,需要强制类型转换。但JDK

50通过泛型很好的解决了这个问题,在一定程度地简化ThreadLocal的使用,代码清单 9 2就使用了JDK

50新的ThreadLocal版本。

概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

Spring使用ThreadLocal解决线程安全问题

 

 我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就

是因为Spring对一些Bean(如RequestContextHolder、

TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态采用

ThreadLocal进行处理,让它们也成为线程安全的状态,因为有状态的Bean就可以在多线程中共享了。

一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程,如图9‑2所示:

这样你就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有关联的对象引用到的都是同一个变量。

由于①处的conn是成员变量,因为addTopic()方法是非线程安全的,必须在使用时创建一个新TopicDao实例(非singleton)。下面使用ThreadLocal对conn这个非线程安全的“状态”进行改造:

代码清单4 TopicDao:线程安全

import javasqlConnection;

import javasqlStatement;

public class TopicDao {

①使用ThreadLocal保存Connection变量

private static ThreadLocal connThreadLocal = new ThreadLocal();

public static Connection getConnection(){

②如果connThreadLocal没有本线程对应的Connection创建一个新的Connection,

并将其保存到线程本地变量中。

if (connThreadLocal get() == null) {

Connection conn = ConnectionManagergetConnection();

connThreadLocalset(conn);

return conn;

}else{

return connThreadLocal get();③直接返回线程本地变量

}

}

public void addTopic() {

④从ThreadLocal中获取线程对应的Connection

Statement stat = getConnection()createStatement();

}

}

 

 不同的线程在使用TopicDao时,先判断connThreadLocal是否是null,如果是null,则说明当前线程还没有对应的

Connection对象,这时创建一个Connection对象并添加到本地线程变量中;如果不为null,则说明当前的线程已经拥有了

Connection对象,直接使用就可以了。这样,就保证了不同的线程使用线程相关的Connection,而不会使用其它线程的

Connection。因此,这个TopicDao就可以做到singleton共享了。

以上就是关于并发编程解惑之线程全部的内容,包括:并发编程解惑之线程、怎样用SQL语句往表里添加数据(使用sql语句向表中添加数据)、如何用eclipse连接mysql数据库等相关内容解答,如果想了解更多相关内容,可以关注我们,你们的支持是我们更新的动力!

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/sjk/10155864.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-05-05
下一篇 2023-05-05

发表评论

登录后才能评论

评论列表(0条)

保存