这是我个人对synchronized简单理解,我们都知道多线程在访问同一个资源时,为了保证数据的安全性,我们一般使用synchronized去给资源加锁,但是锁在加在哪里?线程怎么访问该资源,这些,都是摆在我们面前的问题,我们必须要正视这个问题,因而,经过这么多天的研究,我才慢慢地品出其中的道理,如果有什么不对的地方,希望大家多多指教。
再说synchronized之前,我们需要说说volatile这个保留字,它的意思是“可变的,不稳定的”。为什么说这个保留字呢?因为java内存模型中的可见性、原子性、有序性。每个线程都有各自的内存空间,如果每个线程都去操作主线程的某个变量,当它会吧该变量装载到自己的内存中,而其他线程并不知道,该变量已经改变了。因为每个线程都是相对独立的,并不知道还有别的线程存在。因而,当一个线程对主内存的数据进行操作时,在操作该数据时,必须为该数据进行加锁,不能让别的线程去访问,直到该线程使用完该数据,释放锁后,别的线程才能访问。
因而,我们可以使用volatile去修饰类的成员变量(属性),不能修饰类的方法和类。一旦用这个保留字去修饰的话,那么每个线程去使用完这个变量之后,会立即将其送还给主内存,供其他的线程去使用,当然,我们也可以使用synchronized去修饰,但synchronized只能修饰修饰类的方法和同步块,而不能修饰变量和类。但是synchronized的开销太大,因而,可以在多个线程访问的成员变量上使用volatile,但是要访问的变量已在synchronized代码块中,或者为常量时,不必使用。
volatile使用的三个说明:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)它无法保证某个成员属性(资源)的原子性。
2)禁止进行指令重排序,进而保证java内存模型中的有序性。
ps:java是面向对象的语言,我会以面向对象的思想,去分析这个问题。
同步的前提是至少有一个以上的线程去访问同一个资源,就像是购买火车票一样,很多人同时去买火车票这个对象,我们不去在乎火车票对象的其他属性,只在乎火车票对象的数量这个属性。我们怎么才能去访问这个属性呢?我们都知道多线程实现的方式,一般有两种情况,一时继承Thread类,然后重写其run方法,另外一种,就是实现runnable接口,重写run方法,并通过线程的代理,来执行该实现类。
线程怎么访问火车票对象的资源?我们先看一段普通代码:
package com.zbygroup.booklib.config;
import java.io.IOException;
import java.io.InputStream;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
public class DBConnection {
/**
* 通过配置文件configurationl.xml 创建SqlSessionFactory
*
* @return
* @throws IOException
*/
private static SqlSessionFactory getsqlSessionFactory() {
String resource="configuration.xml";
InputStream inputStream=null;
try {
inputStream =Resources.getResourceAsStream(resource);
} catch (IOException e) {
e.printStackTrace();
}
return new SqlSessionFactoryBuilder().build(inputStream);
}
/**
* 通过SqlSession 获取studentmapper.xml文件中的id值来获取SQL语句,执行SQL语句
*
* @return
* @throws IOException
*/
public static SqlSession getSqlSession() {
return getsqlSessionFactory().openSession();
}
}
上面一段代码有什么用呢?我们需要SqlSessionFactory来创建一个会话对象,通过会话对象openSession()来获取SqlSession对象,通过SqlSession来获取Connection对象,而Connection对象正是连接数据库的对象。我们通过私有方法来创建SqlSessionFactory对象,但我们怎么获取SqlSession对象,从而获取Connection对象,因而,我们可以在另一个类中,通过DBConnection调用静态方法SqlSession 来获取该Connection对象,比如以下的代码:
public class ImpleDao<T> implements BaseDao<T> {
@Override
public int insert(String mapper, T t) {
SqlSession session = DBConnection.getSqlSession();
int line = session.insert(mapper, t);
SQLSessionExecute.executeCommitClose(session);
return line;
}
}
通过DBConnection.getSqlSession();来获取Connection对象,也就是,获取连接数据库的资源。因而,我们常说的获取资源,一般是调用对象的方法或其他,来返回指向该资源(对象)的引用。
我们也可以结合db.properties来说明,我们通过方法来返回properties对象,也就是导入properties中的数据,也就是说获取properties的资源。
如果我们把这种思想放进线程中,我们就可以清晰的发现,所谓的多个线程访问同一资源,也就是获取该资源的引用。通过类的实例对象来调用对象的方法,来获取对象的属性值(也是成员变量的值),从而操作属性的值,这就是所谓的访问资源。
那么问题又来了,我们在哪里使用对象的资源(方法),不论是继承Thread类,还是实现runnable接口,其内部必定要重写run方法。这是线程的执行体,我们千万不要调用这个run方法,如果调用了run方法,你会发现,该线程和普通分支没什么两样。如果我们不是使用线程,一般是线性的执行程序,我们如果使用了多个线程,每个线程有自己的、栈、堆、程序计数器等,而main函数就是一个主线程。
因而,我们可以在run方法中,调用我们需要的对象的方法,来进行设置其属性值,或返回需要的属性值,或进行数据的传递。这就像我们在普通方法中,调用其他的方法,来返回或传递对象的引用,间接地说是获取资源。
但唯一摆在我们面前的是,我们在run方法中,为什么要调用某对象的方法?我们调用这个方法,能够返回给我们什么对象(内容),或者去设置什么对象(内容)?如果我明白了这些,我们就好操作了。
我们要把锁(synchronized)加在获取某个对象的属性的方法上。我们为什么要在对象中加锁,而不是在线程中加锁?因为,如果某个线程访问该对象,或者该对象的方法,就把该对象给锁住,不让其他线程去访问。因为每个线程都是相互独立的个体,彼此不知道除了自己之外,还有其他线程的存在,因而,每个线程都有访问该资源的执行体,如果我们不告诉其他线程,现在已经有一个线程获取该对象了,正在通过该对象的方法或属性,获取该对象的属性值(也可以说资源,属性值就是资源),或在传递其他对象的属性值(也可以说资源,属性值就是资源)。因而,一旦有线程访问该对象(资源),我们就把该对象锁起来,知道线程执行完毕,(比如通过当前线程的挂起,例如Thread.sleep(millis);该线程处于中断状态,其他就绪状态的线程,立即抢占访问该对象,该对象就立即加锁了,其他线程就就无法访问了,这就好比在一般情况下,很多精子去访问卵细胞,一旦有一个精子进入到卵细胞,卵细胞立即发生透明带反应,阻止其他精子的进入卵细胞,这就保证受精的安全性。这难道不像是线程的加锁吗?精子好比很多线程,卵细胞表示唯一的资源,很多线程访问该资源(对象)时,该对象就必须立即加锁,直到该线程离开,其他线程才有机会访问,因而,该线程离开后,也可能立即再次访问该资源。这就是我们为什么要在唯一的对象中加锁,而不是在线程中加锁的原因。
既燃对象加锁了,线程如何访问该对象呢?我们可以通过组合的方式,比如:
我们可以通过线程的构造器,初始化该线程对象时,就把具有资源的类对象的实例加载进来,这和普通类与类调用的组合关系,没有什么区别的。
说了这么多,我们来观察一个例子,就应该没什么问题了:
package mythread;
import java.text.SimpleDateFormat;
import java.util.Date;
public class MyThreadWithSynchronized {
public MyThreadWithSynchronized() {
}
public static void main(String[] args) throws InterruptedException {
// 创建没有synchronized对象锁唯一的资源
System.out.println("创建没有synchronized对象锁唯一的资源");
MyThreadWithSynchronized myThread = new MyThreadWithSynchronized(); // 创建父对象
// 创建子对象
MyThreadWithSynchronized.VirtureResourceWithSynchronized resourceWithSynchronized = myThread.new VirtureResourceWithSynchronized();
// 在主线程main创建三个线程,来查看其之顺序
System.out.println("在主线程main创建三个线程,来查看其之顺序");
Thread threada = new Thread(myThread.new Threada(
resourceWithSynchronized)); // 新建线程
Thread threadb = new Thread(myThread.new Threadb(
resourceWithSynchronized));
Thread threadc = new Thread(myThread.new Threadc(
resourceWithSynchronized));
// 启动线程,使线程达到就绪态
threadc.start();
Thread.sleep(1000);
threada.start();
Thread.sleep(1000); // 主线程挂起一秒钟
threadb.start();
}
/**
*
*
* @author baoya
*
*/
class VirtureResourceWithSynchronized {
private String name;
public void setName(String name) {
this.name = name;
}
public synchronized void addName(int i) {
switch (i) {
case 0:
System.out.println(this.name + "挂起前的时间:"
+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
System.out.println(this.name);
try {
Thread.sleep(2000);
System.out.println(this.name
+ "抢占cpu挂起后的时间:"
+ new SimpleDateFormat("HH:mm:ss")
.format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
break;
case 1:
System.out.println(this.name + "挂起前的时间:"
+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
System.out.println(this.name);
try {
Thread.sleep(2000);
System.out.println(this.name
+ "抢占cpu挂起后的时间:"
+ new SimpleDateFormat("HH:mm:ss")
.format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
break;
default:
System.out.println(this.name + "挂起前的时间:"
+ new SimpleDateFormat("HH:mm:ss").format(new Date()));
System.out.println(this.name);
try {
Thread.sleep(2000);
System.out.println(this.name
+ "线程 b 抢占cpu挂起后的时间:"
+ new SimpleDateFormat("HH:mm:ss")
.format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
break;
}
}
}
/**
*
* 利用组合关系去访问VirtureResourceWithSynchronized对象
*
* 也就是我们常说的去访问该资源
*
* @author
*
*/
class Threada implements Runnable {
private VirtureResourceWithSynchronized resourceWithSynchronized;
public Threada(VirtureResourceWithSynchronized resourceWithSynchronized) {
this.resourceWithSynchronized = resourceWithSynchronized;
}
public void run() {
resourceWithSynchronized.setName("线程a");
resourceWithSynchronized.addName(0);
}
}
/**
* 同理,定义线程b
*
* @author
*
*/
class Threadb implements Runnable {
private VirtureResourceWithSynchronized resourceWithSynchronized;
public Threadb(VirtureResourceWithSynchronized resourceWithSynchronized) {
this.resourceWithSynchronized = resourceWithSynchronized;
}
public void run() {
resourceWithSynchronized.setName("线程b");
resourceWithSynchronized.addName(1);
}
}
/**
* 同理,定义线程c
*
* @author
*
*/
class Threadc implements Runnable {
private VirtureResourceWithSynchronized resourceWithSynchronized;
public Threadc(VirtureResourceWithSynchronized resourceWithSynchronized) {
this.resourceWithSynchronized = resourceWithSynchronized;
}
public void run() {
resourceWithSynchronized.setName("线程c");
resourceWithSynchronized.addName(2);
}
}
}
通过这个例子可以看出来,具有唯一资源的类VirtureResourceWithSynchronized的实例对象resourceWithSynchronized就是作为线程的属性,这就是我们常说的软件工程中的组合关系,我们在创建线程对象时,就把对象resourceWithSynchronized放在线程中的构造器中。我们为什么使用组合关系,这样使用比较灵活。我们可以在线程体中操作对象的的属性和方法等。
由此,我们回到最初的问题,如果是在购票系统中,我们把人当做一个线程类,该线程类拥有很多属性,我们暂不关注其他属性。它可以在同一时刻,去问火车票这个对象的车票属性,然后火车票这个对象被锁定,其他人(线程对象)就不能购票,也就是处于就绪状态,当人这个线程购买成功后,人这个线程即被挂起,其他人(线程)可以去抢占CPU,然后车票这个对象被锁定,如此往复,直到没票为止。
因此,人可以定义为线程对象,而火车票可以定义为火车票对象。