为什么需要数据共享:
并发运行:线程中并发指一个时间段中多个线程都处于已启动但没有运行结束的状态。多个线程之间默认并发运行,这种运行方式往往会出现交叉的情况。
public class Test {
public static void main(String[] args) {
new CounterThread().start();
new TimeThread().start();
}
}
class CounterThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 2; i++) {
System.out.println("^^^"+new Date());
}
}
}
class TimeThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 2; i++) {
System.out.println("@@@"+new Date());
}
}
}
输出:
如何实现数据共享:
串行运行:使原本并发运行的多个线程实现串行运行,即多线程间同步执行,需要通过对象锁机制来实现,synchronized就是一个利用锁实现线程同步的关键字。
import java.util.Date;
public class Test {
public static void main(String[] args) {
Object object = new Object(); //多线程间共享的数据。
new CounterThread(object).start();
new TimeThread(object).start();
}
}
class CounterThread extends Thread{
Object object;
public CounterThread(Object object) {
super();
this.object = object;
}
@Override
public void run() {
synchronized (object) { //synchronized对象锁。
for (int i = 0; i < 2; i++) {
System.out.println("^^^" + new Date());
}
}
}
}
class TimeThread extends Thread{
Object object;
public TimeThread(Object object) {
super();
this.object = object;
}
@Override
public void run() {
synchronized (object) { //synchronized对象锁。
for (int i = 0; i < 2; i++) {
System.out.println("@@@" + new Date());
}
}
}
}
输出:
分析:
① 通过synchronized关键字实现了线程串行运行:一个线程执行完synchronized 代码块后另一个线程才执行,但是哪个线程先执行synchronized 代码块中的代码无法确定。
② synchronized对象锁:该对象锁是Java中创建的一个对象,该对象可由任意类创建,只要求所创建的对象在多个线程之间共享即可。如果对象锁为全局成员,为了保证该对象在多个线程间共享,该成员往往被private static final修饰。
多线程同步原理:
为什么通过synchronized就能实现多线程间串行运行呢?
① 被synchronized括着的部分就是线程执行临界区,每次仅能有一个线程执行该临界区中的代码:当多个线程中的某个线程先拿到对象锁, 则该线程执行临界区内的代码,其他线程只能在临界区外部等待,当此线程执行完临界区中的代码后,在临界区外部等待的其他线程开始再次竞争以获取对象锁,进而执行临界区中的代码,但只能有一条线程“胜利”。
② 临界区中的代码具有互斥性、唯一性和排它性:一个线程只有执行完临界区中的代码另一个线程才能执行。
例一:
import java.text.*;
import java.util.Date;
public class Test {
public static void main(String[] args) {
Object lockObj = new Object();
new DisplayThread(lockObj).start(); //显示器线程先进入启动状态。
}
}
class DisplayThread extends Thread { //显示器线程。
Object lockObj;
public DisplayThread(Object lockObj) {
this.lockObj = lockObj;
}
@Override
public void run() {
synchronized (lockObj) {
new TimeThread(lockObj).start();
try {
sleep(60000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class TimeThread extends Thread { //时间线程。
Object lockObj;
public TimeThread(Object lockObj) {
this.lockObj = lockObj;
}
@Override
public void run() {
System.out.println("时间线程开始执行......");
synchronized (lockObj) {
DateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
String time = dateFormat.format(new Date());
System.out.println(time);//为什么这行代码60秒左右才会执行?
}
}
}
输出:
分析:
显示器线程与时间线程共享lockObj对象,显示器线程优先进入启动状态,执行run方法,执行同步代码块,创建时间线程并进入启动状态,此时有两种情况:
① 时间线程抢过显示器线程:时间线程输出:“时间线程开始执行…”,进而执行同步代码块,因为该同步代码块中显示器进程的内容还没有结束,所以开始等待60s,进而执行时间线程同步代码块的内容。
② 时间线程没有抢过显示器线程:显示器线程直接进入阻塞状态,阻塞瞬间时间线程执行“时间线程开始执行…”,再进入同步代码块,因为显示器线程同步代码块还在阻塞中,知道60s后,时间线程的同步代码块再运行。
例二:
public class Test {
public static void main(String[] args) {
new CounterThread("线程1").start();
new CounterThread("线程2").start();
}
}
class CounterThread extends Thread{
public CounterThread(String threadName){
super(threadName);
}
@Override
public void run() {
synchronized (this) {//此时临界区中的代码无法实现串行执行,因为此时对象锁在线程1和线程2之间不共享。
for (int i = 0; i < 50; i++) {
System.out.println(getName() + " : " + i);
}
}
}
}
部分输出:
分析:因为this代表CounterThread对象,线程1和线程2是不同的对象,所以不能串行执行。
例三:
public class Test {
public static void main(String[] args) {
Runnable counterThread = new CounterThread();
new Thread(counterThread,"线程1").start();
new Thread(counterThread,"线程2").start();
}
}
class CounterThread implements Runnable {
@Override
public void run() {
synchronized (this) {// 此时临界区中的代码可以实现串行执行,因为此时接口实现类对象充当了对象锁的功能,该对象锁在两个线程之间共享
Thread thread = Thread.currentThread();
for (int i = 0; i < 3; i++) {
System.out.println(thread.getName() + ":" + i);
}
}
}
}
输出:
分析:因为this是同一个counterThread,所以可以实现共享。