resumethread,多线程编程中锁如何保证自己是线程安全的

伏羲号

resumethread,多线程编程中锁如何保证自己是线程安全的?

线程安全问题概述卖票问题分析单窗口卖票

resumethread,多线程编程中锁如何保证自己是线程安全的

一个窗口(单线程)卖100张票没有问题

单线程程序是不会出现线程安全问题的

多个窗口卖不同的票

3个窗口一起卖票,卖的票不同,也不会出现问题

多线程程序,没有访问共享数据,不会产生问题

多个窗口卖相同的票

3个窗口卖的票是一样的,就会出现安全问题

多线程访问了共享的数据,会产生线程安全问题

线程安全问题代码实现模拟卖票案例

创建3个线程,同时开启,对共享的票进行出售

线程安全问题原理分析

线程安全问题产生原理图

分析:线程安全问题正常是不允许产生的,我们可以让一个线程在访问共享数据的时候,无论是否失去了cpu的执行权;让其他的线程只能等待,等待当前线程卖完票,其他线程在进行卖票。

解决线程安全问题办法1-synchronized同步代码块同步代码块:synchronized 关键字可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。

使用synchronized同步代码块格式:

synchronized(锁对象){可能会出现线程安全问题的代码(访问了共享数据的代码)}

代码实现如下:

注意:

代码块中的锁对象,可以使用任意的对象。但是必须保证多个线程使用的锁对象是同一个。锁对象作用:把同步代码块锁住,只让一个线程在同步代码块中执行。同步技术原理分析同步技术原理:

使用了一个锁对象,这个锁对象叫同步锁,也叫对象锁,也叫对象监视器

3个线程一起抢夺cpu的执行权,谁抢到了谁执行run方法进行卖票。

t0抢到了cpu的执行权,执行run方法,遇到synchronized代码块这时t0会检查synchronized代码块是否有锁对象发现有,就会获取到锁对象,进入到同步中执行

t1抢到了cpu的执行权,执行run方法,遇到synchronized代码块这时t1会检查synchronized代码块是否有锁对象发现没有,t1就会进入到阻塞状态,会一直等待t0线程归还锁对象,t0线程执行完同步中的代码,会把锁对象归 还给同步代码块t1才能获取到锁对象进入到同步中执行

总结:同步中的线程,没有执行完毕不会释放锁,同步外的线程没有锁进不去同步。

解决线程安全问题办法2-synchronized普通同步方法同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。

格式:

public synchronized void payTicket(){可能会出现线程安全问题的代码(访问了共享数据的代码)}

代码实现:

分析:定义一个同步方法,同步方法也会把方法内部的代码锁住,只让一个线程执行。

同步方法的锁对象是谁?

就是实现类对象 new RunnableImpl(),也是就是this,所以同步方法是锁定的this对象。

解决线程安全问题办法3-synchronized静态同步方法同步方法:使用synchronized修饰的方法,就叫做同步方法,保证A线程执行该方法的时候,其他线程只能在方法外等着。对于static方法,我们使用当前方法所在类的字节码对象(类名.class)。

格式:

public static synchronized void payTicket(){可能会出现线程安全问题的代码(访问了共享数据的代码)}

代码实现:

分析:静态的同步方法锁对象是谁?

不能是this,this是创建对象之后产生的,静态方法优先于对象

静态方法的锁对象是本类的class属性–>class文件对象(反射)。

解决线程安全问题办法4-Lock锁Lock接口中的方法:

public void lock() :加同步锁。public void unlock() :释放同步锁使用步骤:

在成员位置创建一个ReentrantLock对象在可能会出现安全问题的代码前调用Lock接口中的方法lock获取锁在可能会出现安全问题的代码后调用Lock接口中的方法unlock释放锁代码实现:

分析:java.util.concurrent.locks.Lock接口

Lock 实现提供了比使用 synchronized 方法和语句可获得的更广泛的锁定操作。相比Synchronized,ReentrantLock类提供了一些高级功能,主要有以下3项:

等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。通过lock.lockInterruptibly()来实现这个机制。公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。公平锁、非公平锁的创建方式:

锁绑定多个条件,一个ReentrantLock对象可以同时绑定多个对象。ReenTrantLock提供了一个Condition(条件)类,用来实现分组唤醒需要唤醒的线程们,而不是像synchronized要么随机唤醒一个线程要么唤醒全部线程。ReentrantLock和Synchronized的区别相同点:

它们都是加锁方式同步;都是重入锁;阻塞式的同步;也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的(操作系统需要在用户态与内核态之间来回切换,代价很高,不过可以通过对锁优化进行改善);

C语言中Thread与Task有什么区别?

Thread是C#中最早的多线程模型,后来才推出的Task。微软推出Task的目的,就是要替代Thread,给程序员们提供一种更科学的线程模型。

Thread

Thread是基于delegate的早期线程模型。

固定参数的delegate

支持线程的常规操作,如Start,Join,Abort,Interrupt,Suspend,Resume等等。

使用Thread可以完成大部分的常规线程操作。虽然Thread不如Task强大,但是Thread也有一个使用得非常广泛的API:

Thread.Sleep

尽管设计得好的多线程程序,完全不需要显式地Sleep!

另外,刚才用.Net Framework 4.7.2 创建了一个项目,看了看Thred的API,居然也支持了Yield:

Thread.Yield

更有ResetAbort这种操作:

Thread.ResetAbort

真是不用Thread久已!

Task

Task是基于Action,Func的更加现代的线程模型。支持模板参数,比Thread中的固定参数delegate,在进行数据传递的时候,要更加灵活。

基于Action

Task采用了和ThreadPool类似的调度策略,但是Task在多核心CPU中在表现,要比ThreadPool更好。

在单核心CPU上执行的时候,Thread和Task基本没有太大区别。

Task在线程的控制方面,有更加强大的API支持:

Task通过CancellationToken支持一种线程的取消机制

Task支持Delay操作

Task提供了更完善的异常处理机制

Task自带线程工厂,方便随时创建Task

Task支持Wait WaitAny WaitAll

Task支持WhenAny WhenAll

Task支持ContinueWith,节省线程开销

Task支持Yield操作

Task通过TaskScheduler可以支持线程队列

Task还可以配合 async 和 await 关键字,写出更优雅的多线程程序,用过的人才知道有多香!

。。。 。。。

总之,Task是更加现代的线程管理模型,推荐优先使用Task。

后续我将专门写一批C#中线程相关的文章,欢迎关注:

《C#中多线程的那点事儿-Thread入门》

《C#中多线程的那点事-多线程的代价》

《C#中多线程的那点事-线程池》

《C#中多线程的那点事-锁》

《C#中多线程的那点事-死锁》

。。。。。。

指针与句柄的区别?

句柄是一个32位的整数,实际上是Windows在内存中维护的一个对象内存物理地址列表的整数索引。

因为Windows的内存管理经常会将空闲对象的内存释放掉,当需要访问时再重新提交到物理内存,所以对象的物理地址是变化的,不允许程序直接通过物理地址来访问对象。

程序将想访问的对象的句柄传递给系统,系统根据句柄检索自己维护的对象列表就能知道程序想访问的对象及物理地址了。

句柄是一种指向指针的指针。我们知道,所谓指针是一种内存地址。应用程序启动后,组成这个程序的各对象是驻留在内存的。如果简单地理解,似乎我们只要获知这个内存的首地址,那么就可以随时用这个地址访问对象。

但是,Windos是一个以虚拟内存为基础的操作系统。在这种系统环境下,Windows内存管理器经常在内存中来回移动对象,以此来满足各种应用程序的内存需要。

对象被移动意味着它的地址变化了。如果内存总是如此变化,我们该到哪里去找该对象呢?为了解决这个问题,Windows操作系统为各应用程序腾出一些内存地址,用来专门登记各应用对象在内存中的地址变化,而这个地址本身是不会变的。

Windows内存管理器移动对象在内存中的位置后,把对象新的地址告知这个句柄地址来保存。

这样我们只需要记住这个句柄地址就可以间接地知道对象具体在内存中的哪个位置。

这个地址是在对象装载时由系统分配的,当系统卸载时又释放给系统。但是,必须注意的是,程序每次重新启动,系统不能保证分配给这个程序的句柄还是原来的那个句柄。

而且绝大多数情况下的确不一样。

假如我们把进入电影院看电影看成是一个应用程序的启动运行,那么系统给应用程序分配的句柄总是不一样,这和每次电影院售给我们的门票总是不同的座位是一样的道理。

句柄实际上是一种指向某种资源的指针,但与指针又有所不同:指针对应着一个数据在内存中的地址,得到了指针就可以自由地修改该数据。

Windows并不希望一般程序修改其内部数据结构,因为这样太不安全。

所以Windows给每个使用GlobalAlloc等函数声明的内存区域指定一个句柄(本质上仍是一个指针,但不要直接操作它,如果要操作该段内存,对于GlobalAlloc创建的可移动的内存,需要配合使用GlobalLock、GlobalUnlock),平时你只是在调用API函数时利用这个句柄来说明要操作哪段内存。

HDC 是设备描述表句柄CDC 是设备描述表类你使用CreateThead后函数会返回一个句柄,它代表这个线程。

你可能会调用SetThreadPriority去修改线程的优先级,使用ResumeThread去重新开始一个线程的运行,在调用这些函数时你都需要告诉系统你到底要操作哪个线程,而刚才返回的句柄派上用处了,这些函数的第一个参数就是线程的句柄。

怎么优雅的关闭线程?

有三种方法关闭线程:

1.设置退出标志,使线程正常退出,也就是当run()方法完成后线程终止。

2.使用interrupt()方法中断线程。

3.使用stop方法强行终止线程(不推荐使用,Thread.stop, Thread.suspend, Thread.resume 和Runtime.runFinalizersOnExit 这些终止线程运行的方法已经被废弃,使用它们是极端不安全的!)

发表评论

快捷回复: 表情:
AddoilApplauseBadlaughBombCoffeeFabulousFacepalmFecesFrownHeyhaInsidiousKeepFightingNoProbPigHeadShockedSinistersmileSlapSocialSweatTolaughWatermelonWittyWowYeahYellowdog
评论列表 (暂无评论,77人围观)

还没有评论,来说两句吧...