java 程序中怎么保证多线程的运行安全,java多线程实现

文章 3年前 (2021) admin
0

Q1:java 程序中怎么保证多线程的运行安全?

线程安全问题体现在:原子性:CPU执行过程中一个或多个操作不中断的特性;可见性:一个线程对共享变量的修改,另一个线程可以立即看到顺序:程序按照代码的顺序执行。原因:缓存引起的可见性问题,线程切换引起的原子性问题,编译优化引起的有序性问题。解决方案:原子类,同步和锁定在JDK原子的开始可以解决原子性问题,如同步,易失和锁定,以及可见性问题。

Q2:在 Java 程序中怎么保证多线程的运行安全?

你好,java中的多线程安全主要表现在多个线程一起操作同一个共享变量。只要线程互斥,就能保证线程安全。

Q3:请简要说明java中线程安全是怎么回事?

如果您的代码处于多个线程同时运行的进程中,这些线程可能会同时运行代码。如果每次运行的结果与单线程运行的结果相同,并且其他变量的值与预期的相同,则是线程安全的。或者:一个类或程序提供的接口是对线程的原子操作,或者多个线程之间的切换不会导致接口执行结果的模糊,也就是说我们不需要考虑同步问题。线程安全问题是由全局变量和静态变量引起的。一般来说,如果每个线程中的全局变量和静态变量只有读操作,没有写操作,那么这个全局变量就是线程安全的。如果有多个线程同时执行写操作,一般要考虑线程同步,否则可能会影响线程安全。例如,当数组列表类添加一个元素时,它可能有两个步骤要完成:1。将元素存储在项目[大小]的位置;2.增加“大小”的值。在单线程运行的情况下,如果Size=0,在添加一个元素后,元素在位置0,Size=1;例如,在多线程的情况下,有两个线程,线程a首先将元素存储在位置0。但此时,CPU调度线程A暂停,线程B获得运行的机会。线程B也向这个ArrayList添加元素,因为此时Size仍然等于0(注意,我们假设添加一个元素需要两步,但是线程A只完成了第一步),所以线程B也将元素存储在位置0。然后线程a和线程b继续运行,增加大小的值。好,现在让我们看看数组列表。实际上只有一个元素,存储在位置0,但是大小等于2。这就是“线程不安全”。要将此线程安全类编辑为线程安全类,您必须首先在单线程环境中拥有正确的行为。如果一个类实现正确(这是说它符合规范的另一种方式),那么就没有任何操作序列(读写公共字段和调用公共方法)可以使对象处于无效状态,观察对象是否处于无效状态,或者违反类的任何不变量、前提条件或后置条件。此外,如果一个类是线程安全的,当它被多个线程访问时,它仍然必须如上所述正确地运行,而不管在运行时环境中执行这些线程的时序安排或交错,并且在被调用的代码中没有额外的同步。结果是,在所有线程看来,对线程安全对象的操作是以固定且全局一致的顺序进行的。正确性和线程安全性之间的关系非常类似于描述ACID(原子性、一致性、独立性和持久性)事务时使用的一致性和独立性之间的关系:从特定线程的角度来看,不同线程执行的对象操作是顺序执行的(尽管顺序不确定),而不是并行执行的。线程安全不是一个非真即假的命题。Vector的方法都是同步的,Vector被明确设计为在多线程环境中工作。但是它的线程安全性是有限的,即一些方法之间存在状态依赖(类似的,如果在迭代过程中Vector被其他线程修改,Vector.iterator()返回的迭代器会抛出Concurrentmodificationexception)。对于Java类中常见的线程安全级别,没有一个被广泛接受的分类系统,但是在编写类时,尽可能记录它们的线程安全行为是很重要的。Bloch给出了描述五种线程安全的分类方法:不可变、线程安全、条件线程安全、线程兼容性和线程对立。用不用这个系统都无所谓,只要把线下安全功能记录清楚就行。这个系统有它的局限性——不同类型之间的界限不是100%清晰的,在某些情况下它没有被照顾到——但是这个系统是一个很好的起点。这个分类系统的核心是调用者是否能够或者必须用外部同步包围操作(或者一系列操作)。以下部分描述了线程安全的五个类别。
不可变对象必须是线程安全的,永远不需要额外的同步[1]。因为只要一个不可变的对象构建正确,它的外部可见状态就永远不会改变,也永远不会在不一致的状态下被看到。Java类库中的大多数基本数值类,比如Integer、String和BigInteger,都是不可变的。线程安全线程安全对象具有上面“线程安全”一节中描述的属性——当对象被多个线程访问时,类规范指定的约束仍然有效,无论运行时环境如何安排,线程都不需要任何额外的同步。这个线程安全保证非常严格——很多类,比如Hashtable或者Vector,都不能满足这个严格的定义。条件线程安全条件线程安全类对于单个操作可以是线程安全的,但是某些操作序列可能需要外部同步。条件线程安全最常见的例子是遍历Hashtable或Vector返回的迭代器——这些类返回的快速失败迭代器假设迭代器遍历时底层集合不会改变。为了保证遍历过程中其他线程不会改变集合,迭代线程应该保证对集合有独占访问权,以实现遍历的完整性。一般来说,独占访问由锁的同步来保证——类文档应该指出它是哪个锁(通常是对象的内部监视器)。如果记录一个有条件线程安全的类,不仅要记录它是有条件线程安全的,还要记录哪些操作序列必须被阻止并发访问。用户可以合理地假设其他操作序列不需要任何额外的同步。线程兼容性线程兼容性类不是线程安全的,但是通过正确使用同步,可以在并发环境中安全使用。这可能意味着用一个同步块封装每个方法调用,或者创建一个包装对象,其中每个方法都是同步的(就像Collections.synchronizedList())。)。也可能意味着用 synchronized 块包围某些操作序列。为了最大程度地利用线程兼容类,如果所有调用都使用同一个块,那么就不应该要求调用者对该块同步。这样做会使线程兼容的对象作为变量实例包含在其他线程安全的对象中,从而可以利用其所有者对象的同步。  许多常见的类是线程兼容的,如集合类 ArrayList 和 HashMap 、 java.text.SimpleDateFormat 、或者 JDBC 类 Connection 和 ResultSet 。线程对立  线程对立类是那些不管是否调用了外部同步都不能在并发使用时安全地呈现的类。线程对立很少见,当类修改静态数据,而静态数据会影响在其他线程中执行的其他类的行为,这时通常会出现线程对立。线程对立类的一个例子是调用 System.setOut() 的类。

Q4:spring singleton实例中的变量怎么保证线程安全

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 paperList = papersService.queryPaper(paper,localRowSelection); Page localPage = new Page(page, localRowSelection.getTotalRows(), paperList); return localPage; } service里面的成员变量ibatisEntityDao: @SuppressWarnings("unchecked") @Service("papersService") @Transactional(rollbackFor = { Exception.class }) public class TestPaperServiceImpl implements TestPaperService { @Autowired @Qualifier("ibatisEntityDao") private IbatisEntityDao ibatisEntityDao; private static final String NAMESPACE_TESTPAPER = "com.its.exam.testpaper.model.TestPaper"; private static final String BO_NAME[] = { "试卷仓库" }; private static final String BO_NAME2[] = { "试卷配置试题" }; private static final String BO_NAME1[] = { "试卷试题类型" }; private static final String NAMESPACE_TESTQUESTION="com.its.exam.testpaper.model.TestQuestion"; public List queryPaper(TestPaper paper,RowSelection paramRowSelection) throws EicException{ try { return (List) ibatisEntityDao.queryForListWithPage( NAMESPACE_TESTPAPER, "queryPaper", paper,paramRowSelection); } catch (Exception exception) { exception.printStackTrace(); 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得到,一个是业务对象testPaper.getClass(),得到,通过不同客户端的浏览器访问,可得到下面结论, springIoC容器管理的bean就是单例,因为不同的访问均得到相同的对象【在应用开启的状态下,不重新启动应用下,即在同一次的应用运行中】 -------------------------spring 中的sigleton ,这才是真正的整个应用下面就一个实例:class com.its.exam.testpaper.service.impl.TestPaperServiceImpl$$EnhancerByCGLIB$$584b889d -------------------------spring 中的sigleton ,这才是真正的整个应用下面就一个实例:class com.its.exam.testpaper.service.impl.TestPaperServiceImpl$$EnhancerByCGLIB$$584b889d -------------------------spring 中的sigleton ,这才是真正的整个应用下面就一个实例:class com.its.exam.testpaper.service.impl.TestPaperServiceImpl$$EnhancerByCGLIB$$584b889d -------------------------spring 中的sigleton ,这才是真正的整个应用下面就一个实例:class com.its.exam.testpaper.service.impl.TestPaperServiceImpl$$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 5.0通过泛型很好的解决了这个问题,在一定程度地简化ThreadLocal的使用,代码清单 9 2就使用了JDK 5.0新的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 java.sql.Connection;   import java.sql.Statement;   public class TopicDao {   ①使用ThreadLocal保存Connection变量   private static ThreadLocal connThreadLocal = new ThreadLocal();   public static Connection getConnection(){   ②如果connThreadLocal没有本线程对应的Connection创建一个新的Connection,   并将其保存到线程本地变量中。   if (connThreadLocal. get() == null) {   Connection conn = ConnectionManager.getConnection();   connThreadLocal.set(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共享了。

Q5:java中哪些线程安全

JAVA中进程安全的映射有:Hashtable、synchronizedMap和ConcurrentHashMap。如何在java地图中实现路线安全:1。同步映射是Hashtable,concurrenthashmap。2.您看到的Hashtable是直接向hashmap添加一个锁,concurrenthashmap是将它分成多个分段锁。java代码中的线程安全级别:1。绝对的线程安全。在任何环境下,调用者都不需要考虑额外的同步措施,并且可以保证程序的正确性。这个定义非常严格,在java中很少有符合这个要求的类比。对于实现jsr133规范(java内存模型)的jdk(一般在jdk5.0以上),一般的不变量类对绝地来说都是线程安全的。例如字符串、整数类。一般定义如果一个类中的所有字段都是最终类型,一般认为该类是不变的。不变类绝对是线程安全的。2.相对线程安全在正常情况下,调用者不需要考虑线程同步,大多数情况下可以正常运行。jdk中的大多数类都相对安全。最常见的例子是java中的Vector类。

Q6:在一个JAVA多线程程序中,在什么情况下将会调用线程的yield()方法?

开发者对在线Java多线程程序设计的详细分析一、理解多线程是一种允许程序中多个指令流并发执行的机制,每个指令流称为一个线程,相互独立。线程,也称为轻量级进程,像进程一样具有独立的执行控制,并由操作系统调度。不同的是,线程没有独立的存储空间,而是在自己的进程中与其他线程共享一个存储空间,这使得线程之间的通信比进程简单得多。多个线程的执行是并发的,即逻辑上“同时”的,而不管物理上是否“同时”。如果系统只有一个CPU,就不可能“同时”。但是,因为CPU非常快,用户感觉不到差别,所以我们不需要在意,只需要假设每个线程同时执行即可。多线程与传统单线程在编程上最大的区别在于,每个线程的控制流是相互独立的,这使得每个线程之间的代码执行顺序混乱。由此产生的问题,如线程调度和同步将在后面讨论。二:要在Java中实现多线程,我们不妨想象一下创建一个新线程需要做些什么。显然,我们必须指定这个线程要执行的代码,这就是我们在Java中实现多线程所需要做的一切!太神奇了!Java是如何做到这一点的?及格课!作为一种完全面向对象的语言,Java提供了java.lang.Thread类来方便多线程编程。这个类提供了很多方法来帮助我们控制自己的线程。我们未来的讨论将集中在这门课上。那么如何为Java提供我们想要线程化的代码呢?让我们看看线程类。Thread类最重要的方法是run(),它由Thread类的方法start()调用,并提供我们的线程要执行的代码。要指定我们自己的代码,只需覆盖它!方法1:继承Thread类并重写方法run()。我们在创建的Thread类的子类中重写run(),并添加要由线程执行的代码。下面是一个例子:公共类my thread扩展thread {int count=1,numberpublic MyThread(int num){ num=num;system . out . println(" create thread "编号);} public void run(){ while(true){ system。out . println(" thread " number " : count " count);if(count==6)返回;} } public static void main(String args[]){ for(int I=0;I5;I)新MyThread(i 1)。start();}}这个方法简单明了,符合大家的习惯。但是它也有一个很大的缺点,那就是如果我们的类已经继承了一个类(比如Applet类),我们就不能再继承Thread类了。如果我们不想建立一个新的班级,我们应该做什么?让我们探索一种新的方法:我们不创建Thread类的子类,而是直接使用它,因此我们只能将我们的方法作为参数传递给Thread类的实例,这有点类似于回调函数。但是Java没有指针,所以我们只能传递包含这个方法的类的一个实例。那么如何限制这个类包含这个方法呢?当然是用界面啦!(抽象类虽然也可以满足,但是需要继承,我们采用这种新方法的原因不是为了避免继承带来的局限性吗?)Java提供了java.lang.Runnable接口来支持这个方法。方法2:只有一个方法run()来实现Runnable接口。我们声明我们的类实现了Runnable接口,提供了这个方法,并将我们的线程代码写入其中,从而完成了这部分任务。但是Runnable接口对线程没有任何支持,我们还必须创建Thread类的一个实例,这是通过Thread类的构造函数,public Thread(Runnable目标)来实现的;意识到。
下面是一个例子:公共类my thread实现runnable {int count=1,numberpublic MyThread(int num){ num=num;system . out . println(" create thread "编号);} public void run(){ while(true){ system。out . println(" thread " number " : count " count);if(count==6)返回;} } public static void main(String args[]){ for(int I=0;I5;I)新线程(新MyThread(i 1))。start();}}严格来说,创建Thread子类的一个实例也是可行的,但必须注意的是,子类一定不能覆盖Thread类的run方法,否则,线程会执行子类的run方法,而不是我们用来实现Runnable接口的类的run方法,不妨试试。使用Runnable接口实现多线程使我们能够将所有代码都包含在一个类中,有利于封装。它的缺点是我们只能使用一组代码。如果我们想创建多个线程并让每个线程执行不同的代码,我们必须创建额外的类。如果是的话,在大多数情况下也许还不如直接用多个类分别继承 Thread 来得紧凑。 综上所述,两种方法各有千秋,大家可以灵活运用。 下面让我们一起来研究一下多线程使用中的一些问题。 三、线程的四种状态 1. 新状态:线程已被创建但尚未执行(start() 尚未被调用)。 2. 可执行状态:线程可以执行,虽然不一定正在执行。CPU 时间随时可能被分配给该线程,从而使得它执行。 3. 死亡状态:正常情况下 run() 返回使得线程死亡。调用 stop()或 destroy() 亦有同样效果,但是不被推荐,前者会产生异常,后者是强制终止,不会释放锁。 4. 阻塞状态:线程不会被分配 CPU 时间,无法执行。 四、线程的优先级 线程的优先级代表该线程的重要程度,当有多个线程同时处于可执行状态并等待获得 CPU 时间时,线程调度系统根据各个线程的优先级来决定给谁分配 CPU 时间,优先级高的线程有更大的机会获得 CPU 时间,优先级低的线程也不是没有机会,只是机会要小一些罢了。 你可以调用 Thread 类的方法 getPriority() 和 setPriority()来存取线程的优先级,线程的优先级界于1(MIN_PRIORITY)和10(MAX_PRIORITY)之间,缺省是5(NORM_PRIORITY)。 五、线程的同步 由于同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突这个严重的问题。Java语言提供了专门机制以解决这种冲突,有效避免了同一个数据对象被多个线程同时访问。 由于我们可以通过 private 关键字来保证数据对象只能被方法访问,所以我们只需针对方法提出一套机制,这套机制就是 synchronized 关键字,它包括两种用法:synchronized 方法和 synchronized 块。 1. synchronized 方法:通过在方法声明中加入 synchronized关键字来声明 synchronized 方法。如: public synchronized void accessVal(int newVal);synchronized 方法控制对类成员变量的访问:每个类实例对应一把锁,每个 synchronized 方法都必须获得调用该方法的类实例的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。 这种机制确保了同一时刻对于每一个类实例,其所有声明为 synchronized 的成员函数中至多只有一个处于可执行状态(因为至多只有一个能够获得该类实例对应的锁),从而有效避免了类成员变量的访问冲突(只要所有可能访问类成员变量的方法均被声明为 synchronized)。 在 Java 中,不光是类实例,每一个类也对应一把锁,这样我们也可将类的静态成员函数声明为 synchronized ,以控制其对类的静态成员变量的访问。 synchronized 方法的缺陷:若将一个大的方法声明为synchronized 将会大大影响效率,典型地,若将线程类的方法 run() 声明为 synchronized ,由于在线程的整个生命期内它一直在运行,因此将导致它对本类任何 synchronized 方法的调用都永远不会成功。当然我们可以通过将访问类成员变量的代码放到专门的方法中,将其声明为 synchronized ,并在主方法中调用来解决这一问题,但是 Java 为我们提供了更好的解决办法,那就是 synchronized 块。 2. synchronized 块:通过 synchronized关键字来声明synchronized 块。语法如下: synchronized(syncObject){ //允许访问控制的代码 }synchronized 块是这样一个代码块,其中的代码必须获得对象 syncObject (如前所述,可以是类实例或类)的锁方能执行,具体机制同前所述。由于可以针对任意代码块,且可任意指定上锁的对象,故灵活性较高。 六、线程的阻塞 为了解决对共享存储区的访问冲突,Java 引入了同步机制,现在让我们来考察多个线程对共享资源的访问,显然同步机制已经不够了,因为在任意时刻所要求的资源不一定已经准备好了被访问,反过来,同一时刻准备好了的资源也可能不止一个。为了解决这种情况下的访问控制问题,Java 引入了对阻塞机制的支持。 阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪),学过操作系统的同学对它一定已经很熟悉了。Java 提供了大量方法来支持阻塞,下面让我们逐一分析。 1. sleep() 方法:sleep() 允许 指定以毫秒为单位的一段时间作为参数,它使得线程在指定的时间内进入阻塞状态,不能得到CPU 时间,指定的时间一过,线程重新进入可执行状态。典型地,sleep() 被用在等待某个资源就绪的情形:测试发现条件不满足后,让线程阻塞一段时间后重新测试,直到条件满足为止。 2. suspend() 和 resume() 方法:两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的resume() 被调用,才能使得线程重新进入可执行状态。典型地,suspend() 和 resume() 被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用 resume() 使其恢复。 3. yield() 方法:yield() 使得线程放弃当前分得的 CPU 时间,但是不使线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。调用 yield() 的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程。 4. wait() 和 notify() 方法:两个方法配套使用,wait() 使得线程进入阻塞状态,它有两种形式,一种允许 指定以毫秒为单位的一段时间作为参数,另一种没有参数,前者当对应的 notify() 被调用或者超出指定时间时线程重新进入可执行状态,后者则必须对应的 notify() 被调用。 初看起来它们与 suspend() 和 resume() 方法对没有什么分别,但是事实上它们是截然不同的。区别的核心在于,前面叙述的所有方法,阻塞时都不会释放占用的锁(如果占用了的话),而这一对方法则相反。 上述的核心区别导致了一系列的细节上的区别。 首先,前面叙述的所有方法都隶属于 Thread 类,但是这一对却直接隶属于 Object 类,也就是说,所有对象都拥有这一对方法。初看起来这十分不可思议,但是实际上却是很自然的,因为这一对方法阻塞时要释放占用的锁,而锁是任何对象都具有的,调用任意对象的 wait() 方法导致线程阻塞,并且该对象上的锁被释放。 而调用 任意对象的notify()方法则导致因调用该对象的 wait() 方法而阻塞的线程中随机选择的一个解除阻塞(但要等到获得锁后才真正可执行)。 其次,前面叙述的所有方法都可在任何位置调用,但是这一对方法却必须在 synchronized 方法或块中调用,理由也很简单,只有在synchronized 方法或块中当前线程才占有锁,才有锁可以释放。 同样的道理,调用这一对方法的对象上的锁必须为当前线程所拥有,这样才有锁可以释放。因此,这一对方法调用必须放置在这样的 synchronized 方法或块中,该方法或块的上锁对象就是调用这一对方法的对象。若不满足这一条件,则程序虽然仍能编译,但在运行时会出现IllegalMonitorStateException 异常。 wait() 和 notify() 方法的上述特性决定了它们经常和synchronized 方法或块一起使用,将它们和操作系统的进程间通信机制作一个比较就会发现它们的相似性:synchronized方法或块提供了类似于操作系统原语的功能,它们的执行不会受到多线程机制的干扰,而这一对方法则相当于 block 和wakeup 原语(这一对方法均声明为 synchronized)。 它们的结合使得我们可以实现操作系统上一系列精妙的进程间通信的算法(如信号量算法),并用于解决各种复杂的线程间通信问题。关于 wait() 和 notify() 方法最后再说明两点: 第一:调用 notify() 方法导致解除阻塞的线程是从因调用该对象的 wait() 方法而阻塞的线程中随机选取的,我们无法预料哪一个线程将会被选择,所以编程时要特别小心,避免因这种不确定性而产生问题。 第二:除了 notify(),还有一个方法 notifyAll() 也可起到类似作用,唯一的区别在于,调用 notifyAll() 方法将把因调用该对象的 wait() 方法而阻塞的所有线程一次性全部解除阻塞。当然,只有获得锁的那一个线程才能进入可执行状态。 谈到阻塞,就不能不谈一谈死锁,略一分析就能发现,suspend() 方法和不指定超时期限的 wait() 方法的调用都可能产生死锁。遗憾的是,Java 并不在语言级别上支持死锁的避免,我们在编程中必须小心地避免死锁。 以上我们对 Java 中实现线程阻塞的各种方法作了一番分析,我们重点分析了 wait() 和 notify()方法,因为它们的功能最强大,使用也最灵活,但是这也导致了它们的效率较低,较容易出错。实际使用中我们应该灵活使用各种方法,以便更好地达到我们的目的。 七、守护线程 守护线程是一类特殊的线程,它和普通线程的区别在于它并不是应用程序的核心部分,当一个应用程序的所有非守护线程终止运行时,即使仍然有守护线程在运行,应用程序也将终止,反之,只要有一个非守护线程在运行,应用程序就不会终止。守护线程一般被用于在后台为其它线程提供服务。 可以通过调用方法 isDaemon() 来判断一个线程是否是守护线程,也可以调用方法 setDaemon() 来将一个线程设为守护线程。 八、线程组 线程组是一个 Java 特有的概念,在 Java 中,线程组是类ThreadGroup 的对象,每个线程都隶属于唯一一个线程组,这个线程组在线程创建时指定并在线程的整个生命期内都不能更改。 你可以通过调用包含 ThreadGroup 类型参数的 Thread 类构造函数来指定线程属的线程组,若没有指定,则线程缺省地隶属于名为 system 的系统线程组。 在 Java 中,除了预建的系统线程组外,所有线程组都必须显式创建。在 Java 中,除系统线程组外的每个线程组又隶属于另一个线程组,你可以在创建线程组时指定其所隶属的线程组,若没有指定,则缺省地隶属于系统线程组。这样,所有线程组组成了一棵以系统线程组为根的树。 Java 允许我们对一个线程组中的所有线程同时进行操作,比如我们可以通过调用线程组的相应方法来设置其中所有线程的优先级,也可以启动或阻塞其中的所有线程。 Java 的线程组机制的另一个重要作用是线程安全。线程组机制允许我们通过分组来区分有不同安全特性的线程,对不同组的线程进行不同的处理,还可以通过线程组的分层结构来支持不对等安全措施的采用。 Java 的 ThreadGroup 类提供了大量的方法来方便我们对线程组树中的每一个线程组以及线程组中的每一个线程进行操作。 九、总结 在本文中,我们讲述了 Java 多线程编程的方方面面,包括创建线程,以及对多个线程进行调度、管理。我们深刻认识到了多线程编程的复杂性,以及线程切换开销带来的多线程程序的低效性,这也促使我们认真地思考一个问题:我们是否需要多线程?何时需要多线程? 多线程的核心在于多个代码块并发执行,本质特点在于各代码块之间的代码是乱序执行的。我们的程序是否需要多线程,就是要看这是否也是它的内在特点。 假如我们的程序根本不要求多个代码块并发执行,那自然不需要使用多线程;假如我们的程序虽然要求多个代码块并发执行,但是却不要求乱序,则我们完全可以用一个循环来简单高效地实现,也不需要使用多线程;只有当它完全符合多线程的特点时,多线程机制对线程间通信和线程管理的强大支持才能有用武之地,这时使用多线程才是值得的。 来自:开发者在线

版权声明:admin 发表于 2021年10月24日 上午9:53。
转载请注明:java 程序中怎么保证多线程的运行安全,java多线程实现 | 热豆腐网址之家

相关文章