背景:
银行卡转账:比如我卡里有1000块,我现在要转转账200块给家里人,这时候程序执行拿到1000这个余额,执行1000-200这个计算(**A线程**),
正准备将余额=800赋值的时候,这时候另外有个人给我的账号里面转300块(**B线程**),这时候CPU跑到另外一个线程B去做balance=1000+300
这个动作,做完之后余额balance=1300,这时候CPU跑回到A线程继续执行1000-200的动作,最终balance=800 造成数据不一致
多个线程同时操作同一份数据就会有线程安全的问题
为了更直观的描述线程安全问题,通过下面的案例说明,比如,如下有一个方法打印name中的每个字符,然后打印出name,这时候,启动2个线程A, B , A线程打印name=”zhang_hytc” B线程打印name=”lijia” ,我们看A线程和B线程同时启动,这时候会产生什么?
public class ThaditionalThreadSynchronized {
public static void main(String[] args) {
//要想创建 内部类的实例对象,必须要有外部类的实例对象,如何解决?
new ThaditionalThreadSynchronized().init();
}
public void init(){
final Outputer output = new Outputer();
new Thread(new Runnable() {
public void run() {
while (true){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("current-thread1:" + Thread.currentThread().getName());
output.output("zhangxiaoxiao");
}
}
}).start();
new Thread(new Runnable() {
public void run() {
while (true){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("current-thread2:" + Thread.currentThread().getName());
output.output("lijia");
}
}
}).start();
}
class Outputer{
public void output(String name){
System.out.println("name:" + name);
for(int i =0; iout.println("character:" + name.charAt(i));
}
System.out.println();
}
}
}
出现上述问题的本质原因在于多个线程同时操作了同一份数据,而在本案例中这个同一份数据其实就是output这个对象,因此可以如下这么写
public class ThaditionalThreadSynchronized {
public static void main(String[] args) {
//要想创建 内部类的实例对象,必须要有外部类的实例对象,如何解决? 将逻辑封装到方法中,然后new 出外部类去调用这个方法
new ThaditionalThreadSynchronized().init();
}
public void init(){
//内部类对象在使用的时候要求创建时类型为final
final Outputer output = new Outputer();
new Thread(new Runnable() {
public void run() {
while (true){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
output.output("zhangxiaoxiao");
}
}
}).start();
new Thread(new Runnable() {
public void run() {
while (true){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
output.output("lijia-mylove");
}
}
}).start();
}
class Outputer{
String xxx = "";
public void output(String name){ //因为name多个线程操作的不是同一个对象,所以这样是起不到效果的
System.out.println("name:" + name);
synchronized (xxx) { //因为多个线程是公用output这个对象的,所以xxx是公用的,这样就能起到同步的效果
for(int i =0; iout.println("character:" + name.charAt(i));
}
System.out.println();
}
}
}
}
执行结果:
以上写法从功能上没有问题, 但是仔细想一下,其实两个线程操作的都是output这个对象,因此在该对象中定义xxx=”“其实没必要,因此将采取如下写法:
class Outputer{
public void output(String name){ //因为name多个线程操作的不是同一个对象,所以synchronized(name)这样是起不到效果的
System.out.println("name:" + name);
synchronized (this) {
for(int i =0; iout.println("character:" + name.charAt(i));
}
System.out.println();
}
}
}
synchronized 保护的不是一小段代码,而是整个方法的内容,这时候,output方法如下修改:
public synchronized void output2(String name){
System.out.println("name:" + name);
for(int i =0; iout.println("character:" + name.charAt(i));
}
System.out.println();
}
在一段代码中尽量只用一个synchronized, 如果方法里面又使用了synchronized,很可能会出现死锁
如果按照线程A调用的是output方法,线程B调用的是output2()方法,那么这两个线程能否互斥? 答案是肯定的,
因为两个线程检查的是同一个锁对象, output2() 这个synchronized方法用的就是this对象,output用的也是this对象,说明他们用的监视器对象是一样的,所以不会有问题
上面output和output2使用的是同一个锁对象,没有问题,那么如果将output2方法设置成static方法呢,比如叫做output3()? 那么这时候output和output3能否同步?
这时候需要修改Outputer这个类,修改成如下静态类
static class Outputer{
public void output(String name){ //因为name多个线程操作的不是同一个对象,所以这样是起不到效果的
System.out.println("name:" + name);
synchronized (this) { //因为多个线程是公用output这个对象的,所以xxx是公用的,这样就能起到同步的效果
for(int i =0; iout.println("character:" + name.charAt(i));
}
System.out.println();
}
}
public synchronized void output2(String name){
System.out.println("name:" + name);
for(int i =0; iout.println("character:" + name.charAt(i));
}
System.out.println();
}
public static synchronized void output3(String name){
System.out.println("name:" + name);
for(int i =0; iout.println("character:" + name.charAt(i));
}
System.out.println();
}
}
如何让output()和output3()能够互斥?
我们知道静态方法不需要创建类的实例对象,但是字节码对象已经在内存中了,那么如果需要同步的话,那么output方法是不是只要也改成字节码对象就可以了,答案是的
修改如下:
1. synchronized(obj) 中obj作用就是保证同一时刻只有一个人能拿到这把锁进行操作,obj可以是任意的对象,但必须保证多个线程操作的是同一个锁对象,否则是自己玩自己的,就会出现线程安全问题
2. synchronized 修饰方法的锁是this对象, synchronized修饰静态方法的锁是字节码对象
3. 创建 内部类的实例对象,必须要有外部类的实例对象, 如何创建外部类的实例对象然后去创建内部类的实例对象? 是将创建 内部类对象封装到方法中, 如上述案例中,init方法,如果将init方法中内容放到main()方法中直接创建是报错的
4. 局部内部类和匿名内部类只能访问局部final变量,所以Outputer对象定义的时候要加上final关键字
写在最后的话:
多线程并发编程java中是非常重要的一块,一个开发人员能走多远,就看这些核心基础知识的掌握,以上的案例为多年前看张孝祥老师讲解的java多线程技术课程中的案例,在此说明,感谢张孝祥老师
未完,待续…