您的位置:首页 > 编程语言

04.多线程--06.【同步方式在线程两种创建方式中的可行性】【同步代码块和同步函数的关系】【多线程程序设计思路总结】

2013-09-05 17:50 746 查看

多线程--6

同步方式在线程两种创建方式中的可行性

同步代码块和同步函数的关系 ----Runnable实现子类

 多线程程序设计思路

----------- android培训java培训、java学习型技术博客、期待与您交流! ------------

1.    同步方式在线程两种创建方式中的可行性

由于同步函数或者同步代码块都是通过锁机制来达到线程间的同步的。同时线程间的同步需要的锁对象必须也是线程间的共享数据才可以。

1). 使用Thread子类创建线程中的同步

(1). 在Thread子类中的共享数据

Thread子类中的共享数据必须以Thread子类的静态成员变量的方式才能被多个线程对象进行共享

(2). 在Thread子类中使用同步代码块

[1]. 在Thread子类run方法中使用同步代码块进行线程间的同步

此时就必须在Thread子类定义额外的静态成员变量作为同步代码块锁对象

[2]. 举例1 ----在Thread子类中使用普通成员对象做同步代码块的锁

{1}. Thread子类代码示例

class Ticket extends Thread{
private static int tickNum =10;
private Object objLock =new Object();
public void run(){
while(true){
synchronized(objLock){
if(tickNum <=0)
break;

try {
Thread.sleep(10);
}catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

System.out.println(Thread.currentThread()+". sale: ticket"+ tickNum--);
}
}
}
}


{2}. 测试代码

public class TickeDemoCompare{
public static void main(String[] args){
Threadt1 =new Ticket();
Threadt2 =new Ticket();
Threadt3 =new Ticket();
Threadt4 =new Ticket();

t1.start();
t2.start();
t3.start();
t4.start();
}
}


{3}. 打印结果



从运行结果来看,出现了数据安全的问题,打印出了非正数的票。

原因】每个实例化的线程都有自己的objLock对象副本,因此同步的时候使用不是线程间共享的锁对象,因此违背了同步的两个前提中的一个,因此同步失败,出现数据安全问题。

************************静态解决方式*******************************

[3]. 举例2 ----在Thread子类中使用静态成员对象做同步代码块的锁---修正1

{1}. Thread子类实现代码举例

class TicketII extends Thread{
private static int tickNum =10;
private static Object objLock2 =new Object();
public void run(){
while(true){
synchronized(objLock2){
if(tickNum <=0)
break;

try {
Thread.sleep(10);
}catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

System.out.println(Thread.currentThread()+". sale: ticket"+ tickNum--);
}
}
}
}


{2}. 测试代码举例

public class TickeDemoCompareIII{
public static void main(String[] args){
Threadt1 =new TicketII();
Threadt2 =new TicketII();
Threadt3 =new TicketII();
Threadt4 =new TicketII();

t1.start();
t2.start();
t3.start();
t4.start();
}
}


{3}. 运行结果:数据安全问题已经被解决



***************************非静态解决方式****************************

[4]. 举例3 ----在Thread子类中使用普通成员对象做同步代码块的锁---修正1

{1}. 思路:借鉴使用Runnable接口的实现子类实例来创建线程实例的思路

为Thread子类定义一个接受Object类型的构造函数,在主线程的代码中定义一个Object对象,在构建线程对象的时候,为每一个线程对象都传入这样同一个Object对象。

{2}. Thread子类代码示例

class TicketI extends Thread{
private static int tickNum =10;
private Object objLock;

public TicketI(){}

public TicketI(Object objLock){
super();
this.objLock =objLock;
}

public void run(){
while(true){
synchronized(objLock){
if(tickNum <=0)
break;

try {
Thread.sleep(10);
}catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

System.out.println(Thread.currentThread()+". sale: ticket:"+ tickNum--);
}
}
}
}


{3}. 测试代码示例

public class TickeDemoCompareII{
public static void main(String[] args){
ObjectobjLock =new Object();
Threadt1 =new TicketI(objLock);
Threadt2 =new TicketI(objLock);
Threadt3 =new TicketI(objLock);
Threadt4 =new TicketI(objLock);

t1.start();
t2.start();
t3.start();
t4.start();
}
}


{4}. 运行结果:多线程间的数据安全问题已被解决



(3). 在Thread子类中使用同步函数

[1]. 在Thread子类中使用同步函数的方案分析

【注意】非静态同步函数的锁对象就是调用这个同步函数的对象,即this指向的对象并且锁对象要求必须是多个线程之间共享的。因此调用这个同步函数的this对象必须是多线程之间共享的。

{1}. 直接在Thread子类中定义同步函数并在run方法直接进行调用

这种方法一定是不可行的。

采用倒推式分析:Thread子类对象的run中调用了自定义的同步方法 ----> Thread子类对象的start()方法调用了这个run方法 ----> Thread子类对象是调用的start()方法的最终对象。但是每一个Thread子类对象(线程对象)一定是互不相同的,因此Thread子类中的同步方法、run方法和start方法的this对象在多线程之间一定不是共享的。因此这种方式无法保证锁对象的唯一性,也就无法进行线程同步。

{2}. 采用非静态方式解决办法:借鉴Runnable的实现子类的方式

{2}1. 自定义一个类用来存放同步方法。统一在main方法中定义一个这个类的实例对象,

{2}2. 为Thread子类自定义一个刚才提到的自定义类的普通成员属性和以这个自定义类为参数的构造方法。

{2}3. 在main方法中实例化自定义类的对象并在Thread子类是实例化的时候传入这个自定义类对象。

分析】由于每一个Thread子类实例对象都在构造的时候都接受了同一个自定义类的实例对象,并且在这个Thread子类中的run中通过同一个实例对象来调用相应的同步方法。因此这个自定义类的实例对象就是多线程之间的共享数据,又是同步函数的锁对象,这样就保证了同步需要满足的两个前提。这样同步是成功的。

{3}. 注意无法采用静态同步函数的方式来解决问题。因为run方法是非静态方法所以无法直接通过类名来调用静态同步方法

[2]. 采用自定义类保存同步函数的形式在Thread子类中使用同步函数----错误

使用的例子仍然是多线程卖票程序

{1}. 自定义类SynClass

class SynClass{
public synchronized void sellTicket(int tickNum){
if(tickNum >0){
try {
Thread.sleep(10);
}catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

System.out.println(Thread.currentThread()+". sale: ticket"+ tickNum--);
}
}
}


{2}. Thread实现子类

class TicketIII extends Thread{
private static int tickNum =10;
private SynClass synClassObj;

public TicketIII(){}

public TicketIII(SynClass synClassObj){
super();
this.synClassObj =synClassObj;
}

public void run(){
while(true){
synClassObj.sellTicket(tickNum);
}
}
}


{3}. 测试代码

public class TickeDemoCompareIV{
public static void main(String[] args){
SynClasssynClassObj =new SynClass();
Threadt1 =new TicketIII(synClassObj);
Threadt2 =new TicketIII(synClassObj);
Threadt3 =new TicketIII(synClassObj);
Threadt4 =new TicketIII(synClassObj);

t1.start();
t2.start();
t3.start();
t4.start();
}
}


{4}. 运行结果:死循环



原因】在Thread的run方法中使用synClassObj.sellTicket(tickNum);进行“卖票”。但是tickNum仅仅是将这个值10传给了synClassObj的sellTicket(int
tickNum)方法。在sellTicket方法中进行自减的变量是局部变量tickNum。因此每次循环的时候,传入的都是具有相同副本值10的变量tickNum。因此同步方法中的判断条件一直都满足,因此循环打印10。

[3]. 采用自定义类保存同步函数的形式在Thread子类中使用同步函数----修正1

修正思路:每次传入的时候,都直接在Thread子类中的共享变量tickNum直接自减再传入synClassObj的同步函数中。在同步函数中直接打印结果即可。

{1}. 自定义的SynClass ----修正I

class SynClass{
public synchronized void sellTicket(int tickNum){
if(tickNum >0){
try {
Thread.sleep(10);
}catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

System.out.println(Thread.currentThread()+". sale: ticket"+ tickNum);
}
}
}


{2}. Thread子类代码 ----修正I

class TicketIII extends Thread{
private static int tickNum =10;
private SynClass synClassObj;

public TicketIII(){}

public TicketIII(SynClass synClassObj){
super();
this.synClassObj =synClassObj;
}

public void run(){
while(true){
synClassObj.sellTicket(tickNum--);
}
}
}


【注意】测试代码不变

{3}. 运行结果---数据安全问题解决



[4]. 采用自定义类保存同步函数的形式在Thread子类中使用同步函数----修正2

{1}. 修正思路:将Thread子类中的静态共享数据转移到自定义类中作为普通的成员变量(指ticketNum),其余的保持不变。

{2}. 由于自定义对象Thread子类对象中是共享的所以自定义类对象的一切自身的成员属性也一定是多线程之间的共享数据

{3}. 自定义类SynClass示例代码 ----修正2

class SynClass{
private int tickNum =10;
public synchronized void sellTicket(){
if(tickNum >0){
try {
Thread.sleep(10);
}catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

System.out.println(Thread.currentThread()+". sale: ticket"+ tickNum--);
}
}
}


{2}. Thread子类示例代码 ---修正2

class TicketIII extends Thread{
private SynClass synClassObj;

public TicketIII(){}

public TicketIII(SynClass synClassObj){
super();
this.synClassObj =synClassObj;
}

public void run(){
while(true){
synClassObj.sellTicket();
}
}
}


注意】测试代码不变

{3}. 运行结果:解决了数据安全问题



2). 使用Runnable实现子类对象创建线程中的同步

在日志“黑马程序员--04.多线程--07.【Runnable接口的来历】【Thread类和Runnable接口的关系】【个人总结】”中阐述了Runnable接口的来历。实际上Runnable接口的出现就是为了方便对Thread及其子类的编程。

(1). Runnable接口实现类创建线程对象的优越性回顾

[1]. 在Runnable中的实现子类中的run方法中直接对代码进行同步并访问多线程之间的需要操作的共享数据。

[2]. 不必Thread实现子类通过自定义类 (SynClass)的对象调用被封装到自定义类中的共享数据,并格式化了Thread类或者实现子类的代码
(也就是直接在Thread类或者其子类中直接调用自定义类的成员对象的run方法即可,代码固定)

结论开发中常常使用Runnable接口的实现类创建线程对象替代通过Thread子类创建线程对象的方式

(2). 在Runnable实现子类中的共享数据

[1]. Runnable实现子类中的共享数据的做法

直接在Runnable实现子类中定义普通的成员变量就可以达到这个成员变量因为Runnable实现子类对象被多线程对象共享而共享

[2]. Runnable实现子类及其中的普通的成员属性的共享性

{1}. 给出Runnable实现类代码示例

class TicketRunnable implements Runnable{
private int ticketNum =10;
private Object objLock = new Object();

public void run() {
while(true){
synchronized (objLock) {
if(ticketNum <=0)
break;

try {
Thread.sleep(10);
}catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

System.out.println(Thread.currentThread().getName()+":"+ ticketNum--);
}
}
}
}


{2}. 给出测试代码

Runnable ticket =new TicketRunnableII();
Thread t1 =new Thread(ticket, "window1_sale");
Thread t2 =new Thread(ticket, "window2_sale");
Thread t3 =new Thread(ticket, "window3_sale");
Thread t4 =new Thread(ticket, "window4_sale");

t1.start();
t2.start();
t3.start();
t4.start();


{3}. 此时多线程对象以及Runnable实现子类的对象和属性的共享如图所示:



{3}1. 从图中可以看出Runnable实例对象本身targettarget的ticketNum属性target的objLock属性对象都是四个线程对象t1、t2、t3和t4的共享的数据。

{3}2. Runnable实例对象target本身的共享性导致了自身所有的属性ticketNum和objLock在线程对象上也具有共享性

[3]. Runnable实现子类创建线程对象线程对象间数据共享性结论

{1}. 创建线程实例的时候,向所有线程类构造方法的传入的Runnable实例对象一定是线程实例之间最大的共享对象

{2}. Runnable实例对象所有成员属性同样也是线程实例之间共享对象

(2). 在Runnable实现子类中使用同步代码块

Runnable实现子类run方法中使用同步代码块进行线程间的同步

此时就仅仅在Runnable实现子类定义额外的非静态成员变量作为同步代码块锁对象即可。

注意】此时不再举例,前面的课程中有很多在Runnable接口实现类的run方法中使用同步代码块的例子。

(3). 在Runnable实现子类中使用同步函数

[1]. 在Runnable子类使用同步函数的方案分析

{1}.非静态同步函数的锁对象就是调用这个同步函数的对象,即这个this指向的对象。

{2}.根据锁对象的性质:调用同步函数this对象要求必须是多个线程之间共享的对象。{3}.Runnable实例对象本身线程实例间最大的共享数据Runnable实例对象本身的所有成员属性也是线程实例间的共享数据的结论

【依据以上三点】可以把同步函数直接定义在Runnable实现子类中,也可以把同步函数封装到Runnable实现子类某个引用类型的成员属性

[2]. 把同步函数直接定义在Runnable实现子类中的时候,锁对象就是实例化线程对象时候向Thread类的构造方法Thread(Runnable
target)
传入的Runnable子类对象

[3]. 把同步函数封装到在Runnable实现子类的某个引用类型的成员属性的时候,锁对象就是Runnable子类对象本身的该引用类型的成员属性指向的对象

2.    同步代码块和同步函数的关系----Runnable实现子类

1). 从同步代码块向同步函数转型

(1). 同步代码块的自定义锁向this锁转换----- I

[1]. Runnable实现子类对象本身就是共享数据,所以不必额外在Runnable实现子类中定义额外的共享对象来用作锁对象直接使用this作为锁对象即可

[2]. 转换图例



(2). 同步代码块的this锁对象到Runnable实现子类本身的同步函数----- II

[1]. 将使用this对象作为锁对象所构成的同步代码块直接封装Runnable实现子类的自定义同步方法中。

[2]. 在这个run方法中直接调用本类中的同步方法的时候,就通过this指针。由于Runnable实现子类对象的this指针是多线程实例间共享的对象,所以可以实现同步

[3]. 转换图例



(3). Runnable实现子类本身的同步函数封装到统一的自定义类中----- III

[1]. 将Runnable实现子类的自定义同步方法用到的共享数据一同封装自定义的类中。

[2]. 在Runnable实现子类直接增加封装了同步方法和所用的共享数据的类对象作为Runnable本身的成员变量

[3]. 这样封装的好处 ------增强了代码的维护性

如果有多个同步方法和许多共享数据直接写到Runnable实现子类的话,就会使得线程运行的方法run()和其他同步方法混淆到一起,不利于后期维护。

[4]. 在这个run方法中直接调用本类中的新增的封装了同步函数和所用到的共享变量的类对象的某个同步的方法,由于Runnable实现类对象的成员也是多线程之间的共享数据,因此通过该对象作为锁对象可以实现同步成功

[5]. 转换图例



2). 同步函数和同步代码块的关系总结

(1). 同步函数和同步代码块使用的锁对象的范围

[1]. 同步代码块的锁对象可以是任意类型自定义在Runnable实现子类中的成员对象

[2]. 同步函数的锁对象只能是Runnable实现子类对象本身或者封装了该同步函数的自定义类的实例对象

(2). 同步代码块和同步函数的关系

[1]. 同步代码块同步函数泛化

[2]. 同步函数同步代码块特例

3.    多线程程序设计思路总结

1). 多线程程序设计思路

(1). 多线程需求单线程化

无论多线程的需求或者样子是什么样子的,一定要先设计成单线程的代码

(2). 提取需要“同时运行”的代码块

再次根据这段设计出来的单线程代码哪些地方需要同时运行,就需要把这个需要同时运行的部分封装到多线程需要自己定义的run方法中。

(3). 对提取出来的“同时运行”的代码块进行线程化的类包装

设计好的单线程程序抽取出来的需要同时运行的多个代码块之后,观察一下这些代码块是否执行的内容是一样的。

[1].如果抽取出来的代码块执行的是同一个内容,就封装一个自定义的线程类或者Runnable接口实现类中的run方法

[2]. 如果抽取出来的代码块不是执行的同一个内容那么执行的内容有几种就封装到几个Thread实现子类的run中或者几个Runnable实现子类的run方法中。

一句话执行了几种不同的内容就要有几个不同的Thread子类或者Runnable实现子类与之对应

【经验】直接使用Runnable的实现类进行包装就可以了基本完全替代Thread子类的形式

(4). 单线程中“共享数据”的处理

[1]. 提取共享数据Runnable实现子类

{1}. 如果在提取多个需要“同时运行的代码块”的步骤结束之后,如果发现这几个代码块共同都原来的单线程程序中某个变量进行了使用,那就要对这个单线程的变量进行封装,封装到Runnable接口实现类的作为共享数据。

{2}. 如果将原有的单线程变量封装到Runnable接口的实现子类中,为了仍然保持多线程对这个变量数据的共享,就要把这个变量设计为Runnable实现子类的普通成员变量

[2]. 同步多线程要操作的共享数据

对多线程操作的共享数据必须使用同步代码块或者同步函数进行同步,以消除多线程对共享数据操作的数据安全隐患

(5). 在被抽取出来的代码地方实例化线程并启动

在这些线程代码被抽取出来之后,在原来的位置创建这个线程的实例,并启动这些线程即可实现多线程。

----------- android培训java培训、java学习型技术博客、期待与您交流! ------------
 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: