Java中的多线程(2)

概述:

上一篇文章简单的介绍了什么是线程,以及线程的生命周期,还有创建线程的三种方式。接着本篇文章将总结有关线程同步的相关知识,主要讲解使用synchronized实现线程同步。然后总结Java中锁机制,明确什么是对象锁,什么是类锁。然后下篇文章讲解关于Lock的使用以及与synchronized的对比。

一、线程同步及synchronized关键字

1、在了解synchronized关键字实现线程同步之前我们先看一个程序,明确为什么要实现线程同步。下面我们模拟售票系统实现四个售票点发售某日某列车的100张车票。

(1)通过继承Thread类的方式

package com.jwang.thread;

/**
 * @author jwang
 *
 */
public class ThreadTestOne
{
	public static void main(String[] args) 
	{
	    new MyThirdThread().start();
		new MyThirdThread().start();
		new MyThirdThread().start();
	    new MyThirdThread().start();
		
	}
}
class MyThirdThread extends Thread
{
	private int tickets=100;
	
	public void run()
	{
        while(tickets>0)
		{
		     if(tickets>0)
		     {
		    	 System.out.println(Thread.currentThread().getName()+"is saling ticket"+tickets--);
		     }
		}
	}
}

运行结果如下所示:

分析:上面的程序当中我们创建了4个线程对象,实现了四个线程同步运行的效果,但是从程序的运行结果来看,四个线程各从100-1打印车票,也就是说同一张车票被打印了四次,即四个线程各自卖各自的100张票,而不是去卖共同的100张票,我们需要的是多线程处理同一个资源,,在上面的程序当中,我们创建了四个Thread对象,就等于创建了四个资源,每个Thread对象中都有100张票,每个线程独立的处理自己的资源。这没有达到我们的要求。

(2)通过实现Runnable接口的方式

package com.jwang.thread;

/**
 * @author jwang
 *
 */
public class ThreadTestTwo 
{
	public static void main(String[] args) 
	{  
		MyFourThread tt=new MyFourThread();
	    
		new Thread(tt).start();
		new Thread(tt).start();
		new Thread(tt).start();
		new Thread(tt).start();
		
	}

}

class MyFourThread implements Runnable
{
	private int tickets=100;
	public void run()
	{
        while(tickets > 0)
		{
		     if(tickets>0)
		     {
		    	 System.out.println(Thread.currentThread().getName()+"is saling ticket"+tickets--);
		     }
		}
	}
}

运行结果如下所示:

分析:从运行的结果我们发现,这种方式实现了四个线程处理同一个资源对象。但是这几个线程打印的票的顺序是乱的,为什么会发生这种状况,我们再进一步分析。相对于上面的卖票问题而言,四个线程交替执行,cpu在四个线程之间不断切换,而且切换的时间没有规律可循,随时都可能发生。这就会碰到一种意外。

if(tickets>0)
{
    System.out.println(Thread.currentThread().getName()+"is saling                                                                                 
     ticket"+tickets--);
}

假设tickets的值为1的时候,线程1刚执行完if(tickets>0)这行代码,正准备执行下面的代码,就在这时,操作系统将cou切换到了线程2上执行,此时tickets的值仍为1,线程2执行完上面的两行代码,tickets的值为0后,cpu又切换到了线程1上执行,线程1不会再执行if(tickets>0)代码,因为先前已经比较过了,并且比较结果为真,线程1将继续执行打印代码,此时打印出来的tickets的值已经为0了。即判断的代码执行了一次,执行减的代码却执行了两次。也就是说while循环里面的东西在同一时间只能允许一个线程执行。即具有原子性。Java语言中为了为了保证这种原子性提供了synchronized关键字。下面我们具体来看如何使用synchronized关键字实现线程同步。

使用同步代码块实现线程同步

package com.jwang.thread;

public class SyncTest
{
	public static void main(String[] args)
	{
		SyncRunnable syncRunnable = new SyncRunnable();
		Thread t1 = new Thread(syncRunnable, "thread1");
		Thread t2 = new Thread(syncRunnable, "thread2");
		Thread t3 = new Thread(syncRunnable, "thread3");
		Thread t4 = new Thread(syncRunnable, "thread4");
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}

class SyncRunnable implements Runnable
{
	private int tickets = 100;

	@Override
	public void run()
	{
		while (tickets > 0)
		{
			synchronized (this)
			{
				if (tickets > 0)
				{
					System.out.println(Thread.currentThread().getName() + " is saling ticket:" + tickets--);
				}
			}
		}
	}

}

运行结果部分截图如下所示:

分析:我们发现四个线程交替运行按照100-1的顺序打印出了100张车票。所谓的同步代码块就是将具有原子性的代码放在下面的代码中:

//Object为任意对象,作为监视器
synchronized (Object)
{

}

同一时间内只有一个线程可以执行该代码块中的代码,Object可以是任意对象。上面的示例中我们以this作为监视器,可以理解成这个对象就是一把锁,一个线程获得该锁后才可以执行同步代码块中的代码,与此同时其它线程只能等待该线程执行完毕释放锁后才可能获得该锁被cpu调度执行。

使用同步函数实现线程同步:

package com.jwang.thread;

/**
 * 描述:使用同步函数实现线程同步
 * @author jwang
 *
 */
public class SyncTest
{
	public static void main(String[] args)
	{
		SyncRunnable syncRunnable = new SyncRunnable();
		Thread t1 = new Thread(syncRunnable, "thread1");
		Thread t2 = new Thread(syncRunnable, "thread2");
		Thread t3 = new Thread(syncRunnable, "thread3");
		Thread t4 = new Thread(syncRunnable, "thread4");
		t1.start();
		t2.start();
		t3.start();
		t4.start();
	}
}

class SyncRunnable implements Runnable
{
	private int tickets = 100;

	@Override
	public void run()
	{
		while (tickets > 0)
		{
			//调用同步函数
			sale();
		}
	}
	
	/**
	 * 同步方法
	 */
	public synchronized void sale()
	{
		if (tickets > 0)
		{
			System.out.println(Thread.currentThread().getName() + " is saling ticket:" + tickets--);
		}
	}

}

二、Java中的锁机制

1、对象锁:Java中每一个对象都可以作为实现线程同步的一个锁,这是Java内置的特性,因此我们可以简单的理解成Java中的每个对象都有锁标志,称之为对象锁,也即内置锁。也就是说Java中的每个对象都可以当成一把锁,在需要使用锁的地方发挥锁的作用。而线程同步这种场景刚好可以利用所有Java对象都可以作为一把锁的特性实现同步功能。一般我们在使用synchronized通过同步代码块或者非静态的同步函数来实现线程同步的时候使用的就是对象锁。

2、类锁:类锁也是一个抽象的概念,我们知道静态的同步函数调用时是不需要创建对象的,所以这个时候,同步所依赖的锁就是类锁。类锁是用于类的静态方法或者一个类的class对象上的。我们知道,类的对象实例可以有很多个,但是每个类只有一个class对象,所以不同对象实例的对象锁是互不干扰的,但是每个类只有一个类锁。有一点必须注意的是,其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的。

3、可重入性:当线程执行A类的同步方法时,获得了A类的一个对象锁执行同步方法里面的代码,而A的同步方法中又调用了B类的一个同步方法,这个时候线程在执行到B类的同步方法时,时不需要等待获取B类的对象锁的,可以直接执行B类同步方法里面的代码,这就是内置锁的可重入性。

下面通过一些示例来演示同步以及锁相关的知识

JavaLock.java中分别通过四种方式来演示synchronized的用法

package com.jwang.thread;

public class JavaLock
{
	private int tickets = 10;

	// 定义一个包含synchronized同步代码块的函数,使用对象锁
	public void testMethod1()
	{
		while (tickets > 0)
		{
			// this为当前对象,该同步代码块使用对象锁
			synchronized (this)
			{
				if (tickets > 0)
				{
					System.out.println(Thread.currentThread().getName() + " is saling ticket:" + tickets--);
				}
			}
		}
	}

	public void testMethod2()
	{
		while (this.tickets > 0)
		{
			testMethod2(this);
		}
	}

	// 定义一个用synchronized修饰的普通的同步函数,该同步函数使用对象锁
	public synchronized void testMethod2(JavaLock javalock)
	{
		if (javalock.tickets > 0)
		{
			System.out.println(Thread.currentThread().getName() + " is saling ticket:" + javalock.tickets--);
		}
	}

	// 定义一个包含synchronized同步代码块的函数,使用类锁
	public void testMethod3()
	{
		while (tickets > 0)
		{
			// this为当前对象,该同步代码块使用对象锁
			synchronized (JavaLock.class)
			{
				if (tickets > 0)
				{
					System.out.println(Thread.currentThread().getName() + " is saling ticket:" + tickets--);
				}
			}
		}

	}

	public static void testMethod4()
	{
		JavaLock jl = new JavaLock();
		while (jl.tickets > 0)
		{
			testMethod4(jl);
		}
	}

	// 定义一个静态的用synchronized修饰的同步函数
	public static synchronized void testMethod4(JavaLock javaLock)
	{
		if (javaLock.tickets > 0)
		{
			System.out.println(Thread.currentThread().getName() + " is saling ticket:" + javaLock.tickets--);
		}
	}

}

JavaLockTest.java通过不同的方式测试以上各个同步方式

package com.jwang.thread;

public class JavaLockTest
{

	//定义一个成员变量,作为test4方法中线程的共享资源
	private int i = 50;
	
	public static void main(String[] args)
	{

		JavaLockTest jlt = new JavaLockTest();
		//参数为true表示调用以对象锁作为监视器的同步代码块
		jlt.test1(true);
		//参数为false表示调用以class作为类锁的监视器的同步代码块
		jlt.test1(false);
		//参数为true表示调用以类锁为监视器的静态同步函数
		jlt.test2(true);
		//参数为false表示调用以对象锁为监视器的普通同步函数
		jlt.test2(false);
		//分别用线程调用普通同步函数和静态同步
		jlt.test3();
		//A类的同步函数调用B类的同步函数演示锁的可重入性
		jlt.test4();
	}

	/**
	 *  该方法测试使用对象锁或者类锁实现两个线程同时处理统一资源的情况
	 * @param isClassLock
	 */
	public void test1(final boolean isClassLock)
	{
		final JavaLock javaLock = new JavaLock();

		Runnable runnable = new Runnable()
		{
			@Override
			public void run()
			{
				if (!isClassLock)
				{
					javaLock.testMethod1();
				} 
				else
				{
					javaLock.testMethod3();
				}
			}

		};
		Thread thread1 = new Thread(runnable, "thread1");
		Thread thread2 = new Thread(runnable, "thread2");
		thread1.start();
		thread2.start();
	}

	/**
	 * 
	 * @param isClassLock
	 */
	public void test2(final boolean isClassLock)
	{
		final JavaLock javaLock = new JavaLock();

		Runnable runnable = new Runnable()
		{
			@Override
			public void run()
			{
				if (!isClassLock)
				{
					javaLock.testMethod2();
				} 
				else
				{
					JavaLock.testMethod4();
				}

			}
		};
		Thread thread1 = new Thread(runnable, "thread3");
		Thread thread2 = new Thread(runnable, "thread4");
		thread1.start();
		thread2.start();
	}

	//
	public void test3()
	{
		final JavaLock javaLock = new JavaLock();
		// 创建一个线程使用对象锁
		new Thread(new Runnable()
		{
			@Override
			public void run()
			{
				javaLock.testMethod2();

			}
		}, "thread5").start();
		// 创建一个线程使用类锁
		new Thread(new Runnable()
		{
			@Override
			public void run()
			{
				JavaLock.testMethod4();

			}
		}, "thread6").start();

	}

	/**
	 * 该方法测试锁的可重入性
	 */
	public void test4()
	{
		final JavaLockTest jtl = new JavaLockTest();
		final JavaLock javaLock = new JavaLock();
		Runnable runnable = new Runnable()
		{
			@Override
			public void run()
			{
				
				while(jtl.i > 0)
				{
					test5(javaLock,jtl);
				}
			}
		};
		Thread thread1 = new Thread(runnable, "thread7");
		Thread thread2 = new Thread(runnable, "thread8");
		thread1.start();
		thread2.start();
	}

	/**
	 * 该A类的同步函数调用B类的同步函数
	 * @param javaLock
	 * @param run
	 */
	public synchronized void test5(JavaLock javaLock, JavaLockTest run)
	{
		System.out.println(Thread.currentThread().getName()+"------------------------"+run.i--);
		if(run.i == 30)
		{
			javaLock.testMethod2();
		}
	}

}

执行结果分析:

(1)从第一个测试方法的结果中我们发现,不管是使用对象锁,还是类锁都实现了两个线程处理同一个资源的情况。控制台上两个线程交替着输出10-1。

(2)从第二个测试方法中我们发现当两个线程都使用同一对象锁时,调用普通的同步函数,也轻松的实现了两个线程交替处理同一个资源的情况,控制台上两个线程交替着输出10-1

(3)当第二个测试方法中我们使用类锁时,也即线程中调用静态同步函数时,我们发现并不能实现两个线处理同一个资源的情况,两个线程各自输出10-1。这是因为静态函数中的变量每个线程都会创建一份,所以不会有多个线程处理统一资源的这种情况。

(4)第三个测试方法中,我们分别创建了两个线程,一个使用普通的同步函数,一个使用静态的同步函数。执行的结果就是两个线程分别交替着处理各自的资源,分别交替输出10-1。

(5)第四个方法中我们在JavaLockTest中的同步函数中调用了JavaLock的同步函数,发现线程在进入第一个同步函数后进入第二个同步函数时不需要重新获取锁,这便是可重入性,下边是该测试结果的执行结果

猜你喜欢

转载自blog.csdn.net/m0_38045882/article/details/83176558
今日推荐