- 当多个线程对同一个数据进行操作的时候,就会出现线程安全问题。
- 比如银行转账问题:同一个账户一边进行出账操作(淘宝支付),另一边进行入账操作(别人给自己汇款),此时会因为线程同步带来安全性问题。
- 以下举一个线程安全问题的实例:
两个线程不停地向屏幕输出字符串,A线程输出feifeilover,B线程输出xiaoxin,
所要达到的目的是:屏幕显示完整的字符串。
代码如下:
package com.java;
public class Threadtrodition00 {
public static void main(String[] args) {
new Threadtrodition00().init();
}
private void init() {
final Outputer output = new Outputer();
new Thread(new Runnable() { //线程运行的代码在Runnable对象里面
@Override
public void run() { //run中while循环是为了不停地运行
while(true) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
output.output("feifeilover");
}
}
}).start();
new Thread(new Runnable() { //线程运行的代码在Runnable对象里面
@Override
public void run() { //run中while循环是为了不停地运行
while(true) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
output.output("xiaoxin");
}
}
}).start();//main中启动两个线程
}
class Outputer { // 定义一个内部类,此类为一个输出器
public void output(String string) { // 这个方法是为了把字符串的内容打印到屏幕上
int len = string.length();
for (int i = 0; i < len; i++) {
System.out.print(string.charAt(i));// 把字符一个一个的打印到屏幕
}
System.out.println(""); // 换行
}
}
}
注:内部类不能访问局部变量,为访问局部变量要加final;
静态方法里面不能new内部类的实例对象
-
执行后的代码如下显示
理想状态下我们希望上一个字符串打完以后,在执行别的,从执行后的结果显示,它没有等一个字符串全部输出,cpu却跑去执行另一个线程了;
这就是因为线程不同步,而使两个线程都在使用同一个对象。
- 这里先给出一个声明:
同步(Synchronous) 同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后继的行为。
要从根本上解决上述问题 ,就必须保证两个线程A、B在对i输出操作时完全同步。即在线程A写入时,线程B不仅不能写,同时也不能读。因为在线程A写完之前,线程B读取的一定是一个过期数据
java中,提供了一个重要的关键字synchronized来实现这个功能。它的功能是对同步的代码加锁,使得每一次,只能有一个线程进入同步块,从而保证线程间的安全性(即上面代码的for语句每次应该只有一个线程可以执行)。
Synchrouized关键字常用的几种方法:
1.指定加锁对象:对给定对象加锁,进入同步代码前要获得给定对象的锁。
class Outputer {
String str = "";
public void output(String string) {
int len = string.length();
synchronized (str) { //加锁并传入同一个对象
for(int i=0;i<len;i++) {
System.out.print(string.charAt(i));
}
System.out.println("");
}
}
}//内部类,是一个输出器
用Synchronized实现同步互斥,在锁中一定要是同一个对象。
前面我们提到的A线程是output对象,B线程是output对象。这两个使用的是同一个对象,只需在内部类中加入String xxx = “”;获得Outputer的锁。
由以上代码可以看出锁就是Outputer里面的str。Outputer对象在外部看是output,而在内部看就是this。所以代码可以简化为:
class Outputer {
public void output(String string) {
int len = string.length();
synchronized (this) {
for(int i=0;i<len;i++) {
System.out.print(string.charAt(i));
}
System.out.println("");
}
}
}//内部类,是一个输出器
2 . 直接作用于实例对象:相当于对当前实例加锁,进入同步代码前要获得当前实例的锁
方法返回值前加synchronized(一般一段代码中只用一次synchronized,为了防止死锁)
class Outputer {
public synchronized void output(String string) {
int len = string.length();
for(int i=0;i<len;i++) {
System.out.print(string.charAt(i));
}
System.out.println("");
}
}//内部类,是一个输出器
3.直接作用于静态方法:相当于对当前类加锁,进入同步代码前要获得当前类的锁。
静态同步方法使用的锁是该方法所在的class文件对象
代码如下:
static class Outputer {
public synchronized void output(String string) {
int len = string.length();
for(int i=0;i<len;i++) {
System.out.print(string.charAt(i));
}
System.out.println("");
}
}//内部类,是一个输出器
public static synchronized void output3(String string) {
int len = string.length();
for (int i = 0; i < len; i++) {
System.out.print(string.charAt(i));
}
System.out.println("");
}
注:关键字synchronized可以修饰方法或者以同步块的形式来进行使用,它主要确保多个线程在同一个时刻,只能有一个线程处于方法或者同步块中,它确保了线程对变量访问的可见性和排他性。
经典面试题:
- 子线程循环10次,接着主线程循环100次,接着又回到子线程循环10次,接着再回到主线程又循环100次,如此循环50次。
首先,将子线程和主线程中要同步的方法进行封装,加上同步关键字实现同步。
代码如下:
package com.java;
public class TraditionalThread {
public static void main(String[] args) {
final Business business = new Business(); //创建一个business对象
new Thread(new Runnable() {
@Override
public void run() {
for(int i=1;i<=50;i++) { //来回循环50次
business.sub(i);
}
}
}).start();
for(int i=1;i<=50;i++) {
business.main(i);
}
}
} //先起两个线程,主线程和子线程
class Business { //定义内部类
public synchronized void sub(int i) { //定义子线程 (加锁实现同步)
for(int j=1;j<=10;j++) {
System.out.println("sub "+j +","+"loop of " +i);
}
}
public synchronized void main(int i) { //定义主线程(加锁实现同步)
for(int j=1;j<=100;j++) {
System.out.println("main " + j+","+"loop of " +i);
}
}
}
以上代码实现了两个线程的互斥。
等待/通知机制
- 一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B()调用了对象O的notify()方法,线程A收到通知后从对象O的wait()方法返回,进而执行后续操作。以上两个线程就是通过对象O来完成交互的。
wait与notify实现线程间的通信代码(以上述面试题为例)
class Business { // 定义内部类
private boolean BShould = true;
public synchronized void sub(int i) { // 定义子线程 (加锁实现同步)
if (!BShould) {
try {
this.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
for (int j = 1; j <= 10; j++) {
System.out.println("sub " + j + "," + "loop of " + i);
}
BShould = false;
this.notify();
}
public synchronized void main(int i) { // 定义主线程(加锁实现同步)
if (BShould) {
try {
this.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
for (int j = 1; j <= 100; j++) {
System.out.println("main " + j + "," + "loop of " + i);
}
BShould = true;
this.notify();
}
}
- 在使用wait、notify方法时需要先对调用对象加锁
- notify方法调用后,等待线程依旧不会从wait()返回,需要调用notify()的线程释放锁之后, 等待线程才能有机会从wait()返回。
- wait()返回的前提是获得了调用对象的锁。
注:此系列博客参照张孝祥的java并发视频,以及java高并发程序等书写的,因为本人小白正在努力学习中,对知识掌握的特别的肤浅,如果看到我的博文,有什么不对的地方,或者是对我文章有意见的,可以私信给我,我会一直不断改进的。