虽然多线程编程大大提高了效率,但也带来了一定的隐患。例如,如果两个线程同时向数据库表中插入唯一的数据,则可能会导致向数据库中插入相同的数据。今天我们就一起来讨论一下线程安全的问题,Java中提供了什么样的机制来解决线程安全的问题。 以下为本文内容提纲: 1、线程安全问题什么时候会出现? 2.如何解决线程安全问题? 3。synchronized同步方法或同步块 如有不妥之处还请谅解,欢迎批评指正。1、什么时候会出现线程安全问题? 在单线程编程中不会有线程安全问题,但是在多线程编程中,有可能同时访问同一个资源。这个资源可以是各种类型的资源:一个变量,一个对象,一个文件,一个数据库表等等,当多个线程同时访问同一个资源的时候,就会出现一个问题: 因为执行每个线程的进程是不可控的,很可能导致最终结果与实际愿望相反或直接导致程序错误。更多Java学习魏老师:hua2021ei 举个简单的例子: 现在有两个线程分别从网络读取数据,然后插入到一个数据库表中,要求不能插入重复数据。 插入数据的过程中必须有两个操作: 1)检查数据库中是否存在该数据; 2)存在则不插入;如果它不存在,它将被插入到数据库中间。 如果两个线程分别用thread-1和thread-2表示,在某个时刻,thread-1和thread-2都读取了数据X,那么可能会出现这样的情况: thread-1要检查是否数据X存在于数据库中,然后thread-2也去检查数据库中是否存在数据X。 结果,两个线程都检查数据库中不存在数据X,于是两个线程分别将数据X插入到数据库表中。 这就是线程安全的问题,即当多个线程同时访问一个资源时,程序运行的结果不会是你想看到的结果。 这里,这个资源叫做:临界资源(也叫共享资源)。 也就是说,当多个线程同时访问临界资源(对象、对象中的属性、文件、数据库等)时,可能会出现线程安全问题。 但是,当多个线程执行一个方法时,方法内部的局部变量并不是关键资源,因为方法是在栈上执行的,而Java栈是线程私有的,所以不会有线程安全问题。2、如何解决线程安全问题? 那么一般来说,如何解决线程安全的问题呢? 基本上所有的并发模型在解决线程安全问题时都采用“串行访问临界资源”的方案,即同一时间只有一个线程可以访问临界资源,也称为同步互斥访问。 一般来说,在访问临界资源的代码前加一把锁,访问临界资源后释放锁,让其他线程继续访问。 在Java中,提供了两种方式来实现同步互斥访问:synchronized和Lock。 本文主要介绍synchronized的使用,Lock的使用在下一篇博文中介绍。3、synchronized同步方法或同步块 在了解synchronized关键字的使用方法之前,我们先来看一个概念:mutex,顾名思义:一种可以实现互斥访问的锁。 举个简单的例子:如果在临界资源上加互斥量,当一个线程正在访问临界资源时,其他线程只能等待。 在Java中,每个对象都有一个锁标(monitor),也称为监视器。当多个线程同时访问一个对象时,线程只有获得了该对象的锁才能访问。 在Java中,可以使用synchronized关键字来标记方法或代码块。当一个线程调用对象的synchronized方法或访问synchronized代码块时,该线程获得了该对象的锁,其他线程暂时无法访问该方法。只有在本方法或代码块执行完毕后,该线程才会释放该对象的锁,其他线程才能执行本方法或代码块。 这里有几个简单的例子来说明synchronized关键字的用法: 1.同步方法 在下面的代码中,两个线程调用insertData对象插入数据:123456789101112131415161718192021222324252627282930publicclassTest{publicstaticvoidmain(String[]args){finalInsertDatainsertData=newInsertData();newThread(){publicinsertData.insert(Thread.currentThread());};}。开始();newThread(){publicvoidrun(){insertData.insert(Thread.currentThread());};}.start();}}classInsertData{privateArrayListarrayList=newArrayList();publicvoidinsert(Threadthread){for(inti=0;i<5;i++){System.out.println(thread.getName()+"插入数据"+i);arrayList.add(i);}}} 程序此时的输出为: 表示两个线程同时在执行insert方法。 而如果在insert方法前加上关键字synchronized,运行结果为:12345678910classInsertData{privateArrayListarrayList=newArrayList();publicsynchronizedvoidinsert(Threadthread){for(inti=0;i<5;i++){System.out.println(thread.getName()+"插入数据"+i);arrayList.add(i);}}} 从上面输出的结果说明,Thread-1插入数据是在Thread-0插入数据完成后进行的。说明Thread-0和Thread-1依次执行insert方法。 这是同步方法。 不过有几点需要注意: 1)当一个线程正在访问一个对象的synchronized方法时,其他线程不能访问该对象的其他synchronized方法。原因很简单,因为一个对象只有一把锁。当一个线程获得了该对象的锁后,其他线程无法获得该对象的锁,也就无法访问该对象的其他同步方法。 2)当一个线程正在访问一个对象的synchronized方法时,其他线程就可以访问该对象的非synchronized方法。这样做的原因是简单的。访问非同步方法不需要获取对象的锁。如果一个方法没有用synchronized关键字修饰,就意味着它不会使用临界资源,其他线程可以访问这个方法。 3)如果一个线程A需要访问对象object1的synchronized方法fun1,而另一个线程B需要访问对象object2的synchronized方法fun1,即使object1和object2是同一类型),还有不会有线程安全问题,因为他们访问的对象不同,所以不存在互斥问题。 2.synchronized代码块 synchronized代码块类似如下形式:123synchronized(synObject){} 当这个代码块在一个线程中执行时,该线程会获取对象synObject的锁,这样其他线程就不能同时访问这个代码块。 synObject可以是this,表示获取当前对象的锁,也可以是类中的一个属性,表示获取该属性的锁。 比如上面的insert方法可以改成如下两种形式:123456789101112classInsertData{privateArrayListarrayList=newArrayList();publicvoidinsert(Threadthread){synchronized(this){for(inti=0;i<100;i++){System.out.println(thread.getName()+"插入数据"+i);arrayList.add(i);}}}}12345678910111213classInsertData{privateArrayListarrayList=newArrayList();私有对象对象=新对象();publicvoidinsert(Threadthread){synchronized(object){for(inti=0;i<100;i++){System.out.println(thread.getName()+"插入数据"+i);arrayList.add(i);}}}} 从上面可以看出,synchronized代码块比synchronized方法使用起来灵活多了。因为也许一个方法中只有一部分代码需要同步,如果此时同步整个方法,会影响程序执行的效率。使用synchronized代码块可以避免这个问题,synchronized代码块只能同步需要同步的地方。 另外,每个类也会有一把锁,可以用来控制对静态数据成员的并发访问。 而如果一个线程执行一个对象的非静态synchronized方法,而另一个线程需要执行该对象所属类的静态synchronized方法,此时不会发生互斥,因为访问一个静态synchronized方法占用一个类锁,而访问一个非静态同步方法占用一个类锁。静态synchronized方法占用一个对象锁,所以不存在互斥。看下面这段代码就明白了:1234567891011121314151617181920212223242526272829303132333435publicclassTest{publicstaticvoidmain(String[]Insert;arDatagData)=newfinalvoidmain(String[]Insert;arDatagData)=newThread(){@Overridepublicvoidrun(){insertData.insert();}}。开始();newThread(){@Overridepublicvoidrun(){insertData.insert1();}}.start();}}classInsertData{publicsynchronizedvoidinsert(){System.out.println("执行插入");尝试{Thread.sleep(5000);}catch(InterruptedExceptione){e.printStackTrace();}System.out.println("执行插入完成");}publicsynchronizedstaticvoidinsert1(){System.out.println("执行插入1");System.out.println("执行我nsert1completed");}}执行结果; 第一个线程执行insert方法,不会导致第二个线程执行insert1方法阻塞。 让我们看看synchronized关键字的作用是什么这件事,我们反编译它的字节码看看,下面这段代码反编译后的字节码是:1234567891011121314151617publicclassInsertData{privateObjectobject=newObject();publicvoidinsert(Threadthread){synchronized(object){}}publicsynchronizedvoidinsert1(Threadthread){}publicvoidinsert2(Threadthread){}} 从反编译后的字节码可以看出,synchronized代码块其实多了monitorenter和monitorexit两条指令,当monitorenter指令为执行时,对象的锁计数会加1,执行monitorexit指令时,对象的锁计数会减1。其实这和operati中的pv操作很相似ng系统。操作系统中的PV操作用于控制多个线程对关键资源的访问。对于同步方法,执行线程会识别方法的method_info结构中是否设置了ACC_SYNCHRONIZED标志,然后自动获取对象的锁,调用方法,最后释放锁。如果发生异常,线程自动释放锁。 需要注意一点:对于synchronized方法或者synchronized代码块,当发生异常时,JVM会自动释放当前线程占用的锁,所以不会出现异常导致的死锁。