本文目录:
1.几个基本的概念
2.建立线程的两种方法
3.线程相关的经常使用方法
4.多线程安全问题和线程同步
4.1 多线程安全问题
4.2 线程同步
4.3 同步代码块和同步函数的区别以及锁是什么
4.4 单例懒汉模式的多线程安全问题
5.死锁(DeadLock) javascript
本文涉及到的一些概念,有些是基础知识,有些在后文会展开详细的说明。php
还需须要明确的一个关键点是:CPU对就绪队列中每一个线程的调度是随机的(对咱们人类来讲),且分配的时间片也是随机的(对人类来讲)。css
Java中有两种建立线程的方式。html
建立线程方式一: java
例以下面的代码中,在主线程main中建立了两个线程对象,前后并前后调用start()开启这两个线程,这两个线程会各自执行MyThread中的run()方法。nginx
class MyThread extends Thread {
String name;
String gender;
MyThread(String name,String gender){
this.name = name;
this.gender = gender;
}
public void run(){
int i = 0;
while(i<=20) {
//除了主线程main,其他线程从0开始编号,currentThread()获取的是当前线程对象
System.out.println(Thread.currentThread().getName()+"-----"+i+"------"+name+"------"+gender);
i++;
}
}
}
public class CreateThread {
public static void main(String[] args) {
MyThread mt1 = new MyThread("malong","Male");
MyThread mt2 = new MyThread("Gaoxiao","Female");
mt1.start();
mt2.start();
System.out.println("main thread over");
}
}
上面的代码执行时,有三个线程,首先是主线程main建立2个线程对象,并开启这两个线程任务,开启两个线程后主线程输出"main thread over",而后main线程结束。在开启两个线程任务后,这两个线程加入到了就绪队列等待CPU的调度执行。以下图。由于每一个线程被cpu调度是随机的,执行时间也是随机的,因此即便mt1先开启任务,但mt2可能会比mt1线程先执行,也可能更先消亡。git
建立线程方式二:github
class MyThread implements Runnable {
String name;
String gender;
MyThread(String name,String gender){
this.name = name;
this.gender = gender;
}
public void run(){
int i = 0;
while(i<=200) {
System.out.println(Thread.currentThread().getName()+"-----"+i);
i++;
}
}
}
public class CreateThread2 {
public static void main(String[] args) {
//建立子类对象
MyThread mt = new MyThread("malong","Male");
//建立线程对象
Thread th1 = new Thread(mt);
Thread th2 = new Thread(mt);
th1.start();
th2.start();
System.out.println("main thread over");
}
}
这两种建立线程的方法,无疑第二种(实现Runnable接口)要好一些,由于第一种建立方法继承了Thread后就没法继承其余父类。web
Thread类中的方法:django
isAlive()
:判断线程是否还活着。活着的概念是指是否消亡了,对于运行态、就绪态、睡眠态的线程都是活着的状态。currentThread()
:返回值为Thread,返回当前线程对象。getName()
:获取当前线程的线程名称。setName()
:设置线程名称。给线程命名还可使用构造方法Thread(String thread_name)
或Thread(Runnable r,String thread_name)
。getPriority()
:获取线程优先级。优先级范围值为1-10(默认值为5),相邻值之间的差距对cpu调度的影响很小。通常使用3个字段MIN_PRIORITY、NORM_PRIORITY、MAX_PRIORITY分别表示一、五、10三个优先级,这三个优先级可较大地区分cpu的调度。setPriority()
:设置线程优先级。run()
:封装的是线程开启后要执行的任务代码。若是run()中没有任何代码,则线程不作任何事情。start()
:开启线程并让线程开始执行run()中的任务。toString()
:返回线程的名称、优先级和线程组。sleep(long millis)
:让线程睡眠多少毫秒。join(t1)
:将线程t1合并到当前线程,并等待线程t1执行完毕后才继续执行当前线程。即让t1线程强制插队到当前线程的前面并等待t1完成。yield()
:将当前正在执行的线程退让出去,以让就绪队列中的其余线程有更大的概率被cpu调度。即强制本身放弃cpu,并将本身放入就绪队列。因为本身也在就绪队列中,因此即便此刻本身放弃了cpu,下一次仍是可能会当即被cpu选中调度。但毕竟给了机会给其它就绪态线程,因此其余就绪态线程被选中的概率要更大一些。Object类中的方法:
wait()
:线程进入某个线程池中并进入睡眠态。等待notify()或notifyAll()的唤醒。notify()
:从某个线程池中随机唤醒一个睡眠态的线程。notifyAll()
:唤醒某个线程池中全部的睡眠态线程。这里的某个线程池是由锁对象决定的。持有相同锁对象的线程属于同一个线程池。见后文。
通常来讲,wait()和唤醒的notify()或notifyAll()是成对出现的,不然很容易出现死锁。
sleep()和wait()的区别:(1)所属类不一样:sleep()在Thread类中,wait()则是在Object中;(2)sleep()能够指定睡眠时间,wait()虽然也能够指定睡眠时间,但大多数时候都不会去指定;(3)sleep()不会抛异常,而wait()会抛异常;(4)sleep()能够在任何地方使用,而wait()必须在同步代码块或同步函数中使用;(5)最大的区别是sleep()睡眠时不会释放锁,不会进入特定的线程池,在睡眠时间结束后自动苏醒并继续往下执行任务,而wait()睡眠时会释放锁,进入线程池,等待notify()或notifyAll()的唤醒。
java.util.concurrent.locks包中的类和它们的方法:
Lock类中:
lock()
:获取锁(互斥锁)。unlock()
:释放锁。newCondition()
:建立关联此lock对象的Condition对象。Condition类中:
await()
:和wait()同样。signal()
:和notify()同样。signalAll()
:和notifyAll()同样。线程安全问题是指多线程同时执行时,对同一资源的并发操做会致使资源数据的混乱。
例以下面是用多个线程(窗口)售票的代码。
class Ticket implements Runnable {
private int num; //票的数量
Ticket(int num){
this.num = num;
}
//售票
public void sale() {
if(num>0) {
num--;
System.out.println(Thread.currentThread().getName()+"-------"+remain());
}
}
//获取剩余票数
public int remain() {
return num;
}
public void run(){
while(true) {
sale();
}
}
}
public class ConcurrentDemo {
public static void main(String[] args) {
Ticket t = new Ticket(100);
//建立多个线程对象
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
Thread t3 = new Thread(t);
Thread t4 = new Thread(t);
//开启多个线程使其执行任务
t1.start();
t2.start();
t3.start();
t4.start();
}
}
执行结果大体以下:
以上代码的执行过程大体以下图:
共开启了4个线程执行任务(不考虑main主线程),每个线程都有4个任务:
if(num>0)
;num--
;return num
;System.out.println(Thread.currentThread().getName()+"-------"+remain())
。这四个任务的共同点也是关键点在于它们都操做同一个资源Ticket对象中的num,这是多线程出现安全问题的本质,也是分析多线程执行过程的切入点。
当main线程开启t1-t4这4个线程时,它们首先进入就绪队列等待被CPU随机选中。(1).假如t1被先选中,分配的时间片执行到任务②就结束了,因而t1进入就绪队列等待被CPU随机选中,此时票数num自减后为99;(2).当t3被CPU选中时,t3所读取到的num也为99,假如t3分配到的时间片在执行到任务②也结束了,此时票数num自减后为98;(3).同理t2被选中执行到任务②结束后,num为97;(4).此时t3又被选中了,因而能够执行任务③,甚至是任务④,假设执行完任务④时间片才结束,因而t3的打印语句打印出来的num结果为97;(5).t1又被选中了,因而任务④打印出来的num也为97。
显然,上面的代码有几个问题:(1)有些票没有卖出去了可是没有记录;(2)有的票重复卖了。这就是线程安全问题。
java中解决线程安全问题的方法是使用互斥锁,也可称之为"同步"。解决思路以下:
(1).为待执行的任务设定给定一把锁,拥有相同锁对象的线程在wait()时会进入同一个线程池睡眠。
(2).线程在执行这个设了锁的任务时,首先判断锁是否空闲(即锁处于释放状态),若是空闲则去持有这把锁,只有持有这把锁的线程才能执行这个任务。即便时间片到了,它也不是释放锁,只有wait()或线程结束时才会安全地释放锁。
(3).这样一来,锁被某个线程持有时,其余线程在锁判断后就继续会线程池睡眠去了(或就绪队列)。最终致使的结果是,(设计合理的状况下)某个线程必定完整地执行完一个任务,其余线程才有机会去持有锁并执行任务。
换句话说,使用同步线程,能够保证线程执行的任务具备原子性,只要某个同步任务开始执行了就必定执行结束,且不容许其余线程参与。
让线程同步的方式有两种,一种是使用synchronized(){}
代码块,一种是使用synchronized关键字修饰待保证同步的方法。
class Ticket implements Runnable {
private int num; //初始化票的数量
private Object obj = new Object();
Ticket(int num){
this.num = num;
}
//售票
public void sale() {
synchronized(obj) { //使用同步代码块封装须要保证原子性的代码
if(num>0) {
num--;
System.out.println(Thread.currentThread().getName()+"-------"+remain());
}
}
}
//获取剩余票数
public int remain() {
return num;
}
public void run(){
while(true) {
sale();
}
}
}
class Ticket implements Runnable {
private int num; //初始化票的数量
Ticket(int num){
this.num = num;
}
public synchronized void sale() { //使用synchronized关键字,方法变为同步方法
if(num>0) {
num--;
System.out.println(Thread.currentThread().getName()+"-------"+remain());
}
}
//获取剩余票数
public int remain() {
return num;
}
public void run(){
while(true) {
sale();
}
}
}
使用同步以后,if(num>0)
、num--
、return num
和print(num)
这4个任务就强制具备原子性。某个线程只要开始执行了if语句,它就必定会继续执行直到执行完print(num),才算完成了一整个任务。只有完成了一整个任务,线程才会释放锁(固然,也可能继续判断while(true)并进入下一个循环)。
前面的示例中,同步代码块synchronized(obj){}中传递了一个obj的Object对象,这个obj能够是任意一个对象的引用,这些引用传递给代码块的做用是为了标识这个同步任务所属的锁。
而synchronized函数的本质实际上是使用了this做为这个同步函数的锁标识,this表明的是当前对象的引用。但若是同步函数是静态的,即便用了static修饰,则此时this还没出现,它使用的锁是"类名.class"这个字节码文件对象,对于java来讲,这也是一个对象,并且一个类中必定有这个对象。
使用相同的锁之间会互斥,但不一样锁之间则没有任何影响。所以,要保证任务同步(原子性),这些任务所关联的锁必须相同。也所以,若是有多个同步任务(各自保证本身的同步性),就必定不能都使用同步函数。
例以下面的例子中,写了两个相同的sale()方法,而且使用了flag标记让不一样线程能执行这两个同步任务。若是出现了多线程安全问题,则代表synchronized函数和同步代码块使用的是不一样对象锁。若是将同步代码块中的对象改成this后不出现多线程安全问题,则代表同步函数使用的是this对象。若是为sale2()加上静态修饰static,则将obj替换为"Ticket.class"来测试。
class Ticket implements Runnable {
private int num; //初始化票的数量
boolean flag = true;
private Object obj = new Object();
Ticket(int num){
this.num = num;
}
//售票
public void sale1() {
synchronized(obj) { //使用的是obj标识锁
if(num>0) {
num--;
try{Thread.sleep(1);} catch (InterruptedException i){} //为了确保num--和println()分开,加上sleep
System.out.println(Thread.currentThread().getName()+"===sale1==="+remain());
}
}
}
public synchronized void sale2() { //使用this标识锁
if(num>0) {
num--;
try{Thread.sleep(1);} catch (InterruptedException i){}
System.out.println(Thread.currentThread().getName()+"===sale2==========="+remain());
}
}
//获取剩余票数
public int remain() {
return num;
}
public void run(){
if(flag){
while(true) {
sale1();
}
} else {
while(true) {
sale2();
}
}
}
}
public class Mytest {
public static void main(String[] args) {
Ticket t = new Ticket(200);
//建立多个线程对象
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
//开启多个线程使其执行任务
t1.start();
try{Thread.sleep(1);} catch (InterruptedException i){}
t.flag = false;
t2.start();
}
}
如下是执行结果中的一小片断,出现了多线程安全问题。而若是将同步代码块中的obj改成this,则不会出现多线程安全问题。
Thread-0===sale1===197
Thread-1===sale2===========197
Thread-0===sale1===195
Thread-1===sale2===========195
Thread-1===sale2===========193
Thread-0===sale1===193
Thread-0===sale1===191
Thread-1===sale2===========191
单例饿汉式:
class Single {
private static final Single s = new Single();
private Single(){};
public static Single getInstance() {
return s;
}
}
单例懒汉式:
class Single {
private static Single s = null;
private Single(){};
public static getInstance(){
if(s==null) {
s = new Single();
}
return s;
}
}
当多线程操做单例饿汉式和懒汉式对象的资源时,是否有多线程安全问题?
class Demo implements Runnable {
public void run(){
Single.getInstance();
}
}
以上面的代码为例。当多线程分别被CPU调度时,饿汉式中的getInstance()返回的s,s是final属性修饰的,所以随便哪一个线程访问都是固定不变的。而懒汉式则随着不一样线程的来临,不断new Single()
,也就是说各个线程获取到的对象s是不一样的,存在多线程安全问题。
只需使用同步就能够解决懒汉式的多线程安全问题。例如使用同步方法。
class Single {
private static Single s = null;
private Single(){};
public static synchronized getInstance(){
if (s == null){
s = new Single();
}
return s;
}
}
这样一来,每一个线程来执行这个任务时,都将先判断Single.class这个对象标识的锁是否已经被其余线程持有。虽然解决了问题,但由于每一个线程都额外地判断一次锁,致使效率有所降低。能够采用下面的双重判断来解决这个效率下降问题。
class Single {
private static Single s = null;
private Single(){};
public static getInstance(){
if (s == null) {
synchronized(Single.class){
if (s == null){
s = new Single();
}
return s;
}
}
}
}
这样一来,当第一个线程执行这个任务时,将判断s==null
为true,因而执行同步代码块并持有锁,保证任务的原子性。并且,即便在最初判断s==null
后切换到其余线程了,也没有关系,由于总有一个线程会执行到同步代码块并持有锁,只要持有锁了就必定执行s= new Single()
,在这以后,全部的线程在第一阶段的"s==null"判断都为false,从而提升效率。其实,双重判断的同步懒汉式的判断次数和饿汉式的判断次数几乎相等。
最典型的死锁是僵局问题,A等B,B等A,谁都不释放,形成僵局,最后两个线程都没法执行下去。
例以下面的代码示例,sale1()中,obj锁须要持有this锁才能完成任务总体,而sale2()中,this锁须要持有obj锁才能完成任务总体。当两个线程都开始执行任务后,就开始产生死锁问题。
class Ticket implements Runnable {
private int num;
boolean flag = true;
private Object obj = new Object();
Ticket(int num){
this.num = num;
}
public void sale1() {
synchronized(obj) { //obj锁
sale2(); //this锁
}
}
public synchronized void sale2() { //this锁
synchronized(obj){ //obj锁
if(num>0) {
num--;
try{Thread.sleep(1);} catch (InterruptedException i){}
System.out.println(Thread.currentThread().getName()+"========="+remain());
}
}
}
//获取剩余票数
public int remain() {
return num;
}
public void run(){
if(flag){
while(true) {
sale1();
}
} else {
while(true) {
sale2();
}
}
}
}
public class DeadLockDemo {
public static void main(String[] args) {
Ticket t = new Ticket(200);
//建立多个线程对象
Thread t1 = new Thread(t);
Thread t2 = new Thread(t);
//开启多个线程使其执行任务
t1.start();
try{Thread.sleep(1);} catch (InterruptedException i){}
t.flag = false;
t2.start();
}
}
为了不死锁,尽可能不要在同步中嵌套同步,由于这样很容易形成死锁。
注:若您以为这篇文章还不错请点击右下角推荐,您的支持能激发做者更大的写做热情,很是感谢!