Java 知识点整理-18.多线程

本文详细介绍了Java中的多线程概念,包括线程的定义、并行与并发的区别、线程的实现方式(如继承Thread类和实现Runnable接口)、线程的同步与死锁、线程安全问题以及相关API的使用,如线程的休眠、守护线程和线程的优先级设置等。通过示例代码展示了如何创建和管理线程,并强调了多线程在提高系统效率和资源利用率方面的应用。
摘要由CSDN通过智能技术生成

目录

线程Thread的定义

多线程并行和并发的区别

Java程序运行原理和JVM的启动是多线程的吗

多线程程序实现的方式一

多线程程序实现的方式二

实现Runnable的原理

两种方式的区别

匿名内部类实现线程的两种方式

获取名字和设置名字

获取当前线程的对象

休眠线程

守护线程

加入线程

礼让线程

设置线程的优先级

同步代码块

同步方法

线程安全问题

火车站卖票的例子用实现Runnable接口

死锁

以前的线程安全的类回顾


线程Thread的定义

每个正在系统上运行的程序都是一个进程(可以打开Windows任务管理器查看)。每个进程包含一到多个线程。进程也可能是整个程序或者是部分程序的动态执行。线程是一组指令的集合,或者是程序的特殊段,它可以在程序里独立执行。也可以把它理解为代码运行的上下文,是程序执行的一条路径。所以线程基本上是轻量级的进程,它负责在单个程序里执行多任务。通常由操作系统负责多个线程的调度和执行。线程的运行中需要使用计算机的内存资源和CPU。

线程是程序中一个单一的顺序控制流程。在单个程序中同时运行多个线程完成不同的工作,称为多线程。 同时对多个任务加以控制,就要求程序能够停下手头的工作,改为处理其他一些问题,再返回主进程。程序在逻辑意义上被分割为数个线程,如果操作系统本身支持多个处理器,那么每个线程都可分配给一个不同的处理器,真正进入“并行运算”状态。(利用并发实现了“并行“)

线程和进程的区别在于,子进程和父进程有不同的代码和数据空间,而多个线程则共享数据空间,每个线程有自己的执行堆栈和程序计数器为其执行上下文。

多线程是为了同步完成多项任务,不是为了提高运行效率,而是为了提高资源使用效率来提高系统的效率。所以多线程是在同一时间需要完成多项任务的时候实现的。

多线程的缺点:

如果有大量的线程,会影响性能,因为操作系统需要在它们之间切换。

更多的线程需要更多的内存空间。

线程可能会给程序带来更多“bug”,因此要小心使用。

线程的中止需要考虑其对程序运行的影响。

通常块模型数据是在多个线程间共享的,需要防止线程死锁情况的发生。

多线程的应用场景:

红蜘蛛(多媒体教室系统)同时共享屏幕给多个电脑。

迅雷开启多条线程一起下载。

QQ同时和多个人一起视频。

服务器同时处理多个客户端请求(例如百度)。


多线程并行和并发的区别

并行就是两个任务同时运行,就是甲任务执行的同时,乙任务也在执行。(需要多核CPU)

并发是指两个任务都请求运行,而处理器只能接收一个任务,就把这两个任务安排轮流执行,由于时间间隔较短,使人感觉两个任务都在运行。(单个CPU即可实现)

比如我跟两个网友聊天,左手操作一个电脑跟甲聊,同时右手用另一台电脑跟乙聊天,这就叫并行。

如果用一台电脑我先给甲发个消息,然后立刻再给乙发消息,然后再跟甲聊,再跟乙聊。这就叫并发。


Java程序运行原理和JVM的启动是多线程的吗

Java程序运行原理

Java命令会启动java虚拟机。启动JVM,等于启动了一个应用程序,也就是启动了一个进程。该进程会自动启动一个 “主线程” ,然后主线程去调用某个类的 main 方法。

JVM的启动是多线程的吗

JVM启动至少启动了垃圾回收线程和主线程,所以是多线程的。

演示:

/*
 * 证明jvm是多线程的
 */
public class Demo1_Thread {
	public static void main(String[] args) {	//main方法本身就是一个线程,主线程
		//创建垃圾
		for(int i = 0; i < 580000; i++) {
			new Demo();		//启动一个垃圾回收线程
		}
		
		for (int i = 0; i < 10000; i++) {		//两条线程间隔执行,互相抢夺资源
			System.out.println("我是主线程的执行代码!");
		}
	}
}

class Demo {
	public void finalize(){
		System.out.println("垃圾被清扫了!");
	}
}

 


多线程程序实现的方式一

Thread类概述

public class Thread extends Object implements Runnable 线程是程序中的执行线程。Java虚拟机允许应用程序并发地运行多个执行线程(即java支持多线程)。java.lang包下,使用无需导包。

创建新执行线程有两种方法。一种方法是将此类声明为Thread的子类。该子类应重写Thread类的run方法。接下来可以分配并启动该子类的实例。例如,计算大于某一规定值的质数的线程可以写成:

class PrimeThread extends Thread {
    long minPrime;
    PrimeThread(long minPrime)  {
        this.minPrime = minPrime;
    }
    public void run() {
        //compute primes larger than minPrime
        ...
    }
}

然后,下列代码会创建并启动一个线程:

PrimeThread p = new PrimeThread(143) 
p.start();

 

Thread类的成员方法

public void start() 使该线程开始执行;Java虚拟机调用该线程的run方法。

结果是两个线程并发地运行;当前线程(从调用返回给start方法)和另一个线程(执行其run方法)。

多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。

抛出:IllegalThreadStateException - 如果线程已经启动。

演示:

public class Demo2_Thread {
	public static void main(String[] args) {
		//4.创建线程的子类对象
		MyThread mt = new MyThread();
//		mt.run();	//虽然继承了Thread,重写了run,但并没有开启单独的线程,相当于还在主线程中运行
		//5.开启新线程,虚拟机会默认调用run方法
		mt.start();
		
		for(int i = 0; i < 1000; i++) {
			System.out.println("bb");
		}
	}
}

//1.定义一个类继承Thread
class MyThread extends Thread {		
	//2.重写run方法
	public void run() {				
		//3.将你想要执行的代码写在run方法中
		for(int i = 0; i < 1000; i++) {
			System.out.println("aaaaaaaaaaaaaaa");
		}
	}
}

 

 


多线程程序实现的方式二

Thread类概述

创建线程的另一种方法是声明实现Runnable接口的类该类然后实现run方法。然后可以分配该类的实例,在创建Thread时作为一个参数来传递并启动。采用这种风格的同一个例子如下所示:

class PrimeThread implements Runnable {
    long minPrime;
    PrimeRun(long minPrime)  {
        this.minPrime = minPrime;
    }
    public void run() {
        //compute primes larger than minPrime
        ...
    }
}

然后,下列代码会创建并启动一个线程:

PrimeRun p = new PrimeRun(143);
new Thread(p).start();

每个线程都有一个标识名,多个线程可以同名。如果线程创建时没有指定标识名,就会为其生成一个新名称。

Runnable接口的概述

public interface Runnable,Runnable接口应该由那些打算通过某一线程执行其实例的类来实现。类必须定义一个称为run的无参数方法。

设计该接口的目的是为希望在活动时执行代码的对象提供一个公共协议。例如:Thread类实现了Runnable。激活的意思是说某个线程已启动并且尚未停止。

此外,Runnable为非Thread子类的类提供了一种激活方式。通过实例化某个Thread实例并将自身作为运行目标,就可以运行实现Runnable的类而无需创建Thread的子类。大多数情况下,如果只想重写run(),而不重写其他Thread方法,那么应使用Runnable接口。这很重要,因为除非程序员打算修改或增强类的基本行动,否则不应为该类创建子类。

Runnable接口的构造方法

Thread(Runable target) 分配新的Thread对象。参数只能接收Runnable的子类对象,因为Runnable是个接口。

Runnable接口的方法

void run() 使用实现接口Runnable的对象创建一个线程时,启动该线程将导致在独立执行的线程中调用对象的run方法。

演示:

public class Demo3_Thread {
	public static void main(String[] args) {
		//4.创建Runnable的子类对象
		MyRunnable mr = new MyRunnable();
//		Runnable target = mr;		//父类引用指向子类对象
/*		Thread t = new Thread(mr);	//mr代表Runnable的子类对象
		t.start();*/
		//5.将其当作参数传递给Thread的构造函数,并开启新线程
		new Thread(mr).start();		//MyRunnable里是没有start方法的,所以只能把他当参数传递给Thread的构造方法,通过Thread去开启线程。
		
		for(int i = 0; i < 1000; i++) {
			System.out.println("bb");
		}
	}
}

//1.定义一个类实现Runnable接口
class MyRunnable implements Runnable {
	//2.重写run方法
	public void run() {
		//3.将要执行的代码写在run方法中
		for(int i = 0; i < 1000; i++) {
			System.out.println("aaaaaaaaaaaaaaa");
		}
	}
}


实现Runnable的原理

查看源码

1.看Thread类的构造函数,传递了Runnable接口的引用。

2.通过init()找到传递的target给成员变量的target赋值。

3.查看run方法,发现run方法中有判断,如果target不为null就会调用Runnable接口子类对象的run方法。

 

		/* What will be run. */
	    private Runnable target;		//mr地址值被一层层传入给Runnable target赋值了。mr记录的是Runnable的子类对象new MyRunnable()
	    
		public Thread(Runnable target) {
	        init(null, target, "Thread-" + nextThreadNum(), 0);
	    }
		
		private void init(ThreadGroup g, Runnable target, String name,
                long stackSize) {
			init(g, target, name, stackSize, null, true);
		}
		
		private void init(ThreadGroup g, Runnable target, String name,
                long stackSize, AccessControlContext acc,
                boolean inheritThreadLocals) {
		  if (name == null) {
		      throw new NullPointerException("name cannot be null");
		  }
		
		  this.name = name;
		
		  Thread parent = currentThread();
		  SecurityManager security = System.getSecurityManager();
		  if (g == null) {
		      /* Determine if it's an applet or not */
		
		      /* If there is a security manager, ask the security manager
		         what to do. */
		      if (security != null) {
		          g = security.getThreadGroup();
		      }
		
		      /* If the security doesn't have a strong opinion of the matter
		         use the parent thread group. */
		      if (g == null) {
		          g = parent.getThreadGroup();
		      }
		  }
		
		  /* checkAccess regardless of whether or not threadgroup is
		     explicitly passed in. */
		  g.checkAccess();
		
		  /*
		   * Do we have the required permissions?
		   */
		  if (security != null) {
		      if (isCCLOverridden(getClass())) {
		          security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
		      }
		  }
		
		  g.addUnstarted();
		
		  this.group = g;
		  this.daemon = parent.isDaemon();
		  this.priority = parent.getPriority();
		  if (security == null || isCCLOverridden(parent.getClass()))
		      this.contextClassLoader = parent.getContextClassLoader();
		  else
		      this.contextClassLoader = parent.contextClassLoader;
		  this.inheritedAccessControlContext =
		          acc != null ? acc : AccessController.getContext();
		  this.target = target;				//target被一层层传入,然后赋给了一个成员变量
		  setPriority(priority);
		  if (inheritThreadLocals && parent.inheritableThreadLocals != null)
		      this.inheritableThreadLocals =
		          ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
		  /* Stash the specified stack size in case the VM cares */
		  this.stackSize = stackSize;
		
		  /* Set thread ID */
		  tid = nextThreadID();
		}
		
		//当Runnable target有值了,即这里的target是mr(父类引用指向子类对象,编译看父类,运行看子类)。如果mr不为null,就mr.run()即我们定义类中的run() 所以t.start()其实调用的是该run()
		public void run() {
	        if (target != null) {
	            target.run();
	        }
	    }

两种方式的区别

查看源码的区别:

继承Thread:由于子类重写了Thread类的run(),当调用start()时,直接找子类的run()方法。

实现Runnable:构造函数中传入了Runnable的引用,成员变量记住了它,start()调用run()方法时内部判断成员变量Runnable的引用是否为空。编译时看的是Runnable的run()里判断传入的参数是否为空,不为空就可以调用run(),运行时执行的是子类的run()方法。

继承Thread

好处是:可以直接使用Thread类中的方法,代码简单。

弊端是:如果已经有了父类,就不能用这种方法,java是单继承的。

实现Runnable接口

好处是:即使自己定义的线程类有了父类也没关系,因为有了父类也可以实现接口,而且接口是可以多实现的。拓展性强。

弊端是:不能直接使用Thread中的方法需要先获取到线程对象后,才能得到Thread的方法,代码复杂。

总结:

这两种方法的利弊是相互的,推荐优先考虑继承Thread类,如果他有了父类再考虑实现Runnable接口。


匿名内部类实现线程的两种方式

匿名内部类的好处是不用找一个类去继承Thread类或实现Runnable接口了,直接来一个new Thread或new Runnable就可以了,然后只需要重写他里面的一个run()即可。

继承Thread类

演示:

public class Demo4_Thread {
	public static void main(String[] args) {
		//1.new 类()  {}就是继承这个类。
		new Thread() {
			//2.重写run方法
			public void run() {
				//3.将要执行的代码,写在run方法中
				for(int i = 0; i < 1000; i++) {
					System.out.println("aaaaaaaaaaaaa");
				}
			}
		}.start();		//整个代表Thread的子类对象,所以直接来个.start() start()会调用run(),开启线程
	}
}

实现Runnable接口

演示:

public class Demo4_Thread {
	public static void main(String[] args) {
		new Thread(
				//1.new 接口() {}就是实现这个接口
				new Runnable() {
					//2.重写run方法
					public void run() {
						//3.将要执行的代码,写在run方法中
						for(int i = 0; i < 1000; i++) {
							System.out.println("bb");
						}
					}
				}).start();		//整个代表Runnable接口的子类对象,他没有start(),我们将他当作参数传给Thread的构造方法。再.start() start()会调用run(),开启线程
	}
}

区别

匿名内部类实现继承Thread类,直接继承;实现Runnable接口,需要把Runnable接口的子类对象当作参数传递给Thread构造。


获取名字和设置名字

获取名字

通过getName()可以返回线程对象的名字。

Thread类的成员方法

Public void String getName() 返回该线程的名称。

演示:

public class Demo1_Name {
	public static void main(String[] args) {
		new Thread() {
			public void run() {
				System.out.println(this.getName() + "....aaaaaaaaaaaa");	//this相当于这个匿名内部类对象 Thread-0....aaaaaaaaaaaaa
			}
		}.start();	//开启一条线程
		
		new Thread() {
			public void run() {
				System.out.println(this.getName() + "....bb");	//Thread-1....bb 所以线程有一个默认的名字,编号从0开始
			}
		}.start();	//开启另一条线程
	}
}

设置名字

1.通过构造函数可以传入String类型的名字。

Thread类的构造方法

Thread(String name) 分配新的Thread对象。

演示:

public class Demo1_Name {
	public static void main(String[] args) {
		new Thread("特不靠谱") {
			public void run() {
				System.out.println(this.getName() + "....aaaaaaaaaaaa");	//特不靠谱....aaaaaaaaaaaa
			}
		}.start();
		
		new Thread("普京大帝") {
			public void run() {
				System.out.println(this.getName() + "....bb");	//普京大帝....bb
			}
		}.start();	
	}
}

2.通过setName(String)可以设置线程对象的名字。

Thread类的成员方法

public void setName(String name) 改变线程名称,使之与参数name相同。

演示:

//匿名内部类
public class Demo1_Name {
	public static void main(String[] args) {
		new Thread() {
			public void run() {
				this.setName("梅沙尼亚");	
				System.out.println(this.getName() + "....aaaaaaaaaaaa");
			}
		}.start();
		
		new Thread() {
			public void run() {
				this.setName("特朗普");
				System.out.println(this.getName() + "....bb");
			}
		}.start();
	}
}
//有名字的内部类
public class Demo1_Name {
	public static void main(String[] args) {
		Thread t1 = new Thread() {
			public void run() {
				System.out.println(this.getName() + "....aaaaaaaaaaaa");
			}
		};
		
		Thread t2 = new Thread() {
			public void run() {
				System.out.println(this.getName() + "....bb");
			}
		};
		
		t1.setName("特鲁多");
		t2.setName("撒切尔夫人");
		t1.start();		//特鲁多....aaaaaaaaaaaa
		t2.start();		//撒切尔夫人....bb
	}
}

 


获取当前线程的对象

Thread类的成员方法

public static Thread currentThread() 返回对当前正在执行的线程对象的引用。主线程也可以获取。

演示:

public class Demo2_CurrentThread {
	public static void main(String[] args) {
		new Thread() {
			public void run() {
				System.out.println(getName() + "....aaaaaaaaaaaaaaa");		//Thread-0....aaaaaaaaaaaaaaa
			}
		}.start();
		
		new Thread(new Runnable() {
			public void run() {
				//Thread.currentThread()获取当前正在执行的线程对象 然后再拿到名字
				System.out.println(Thread.currentThread().getName() + "....bb");	//Thread-1....bb
			}
		}).start();
	
		//设置主线程的名字
		Thread.currentThread().setName("我是主线程!");
		//获取主线程的名字
		System.out.println(Thread.currentThread().getName());		//我是主线程!
	}
}

休眠线程

Thread类的成员方法

public static void sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到计时器和调度程序精度和准确性的影响。

public static void sleep(long millis, int nanos) 在指定的毫秒数加指定的纳秒数内让当前正在执行的线程休眠(暂停执行),此操作受到计时器和调度程序精度和准确性的影响。(1秒=1000毫秒,1毫秒=1000微秒,1微秒=1000纳秒,所以1秒 = 1000 * 1000 * 1000纳秒)

演示:

public class Demo3_Sleep {
	public static void main(String[] args) throws InterruptedException {
//		demo_sleepInMainThread();
		new Thread() {
			public void run() {
				for(int i = 0; i < 10; i++) {
					try {
						Thread.sleep(1000);	//父类Thread里的run()是没有抛异常的,则子类在重写父类的方法时就不能抛异常(父亲坏了,儿子不能更坏;父亲没坏,儿子有坏的东西必须自己处理)
					} catch (InterruptedException e) {
						e.printStackTrace();
					}		
					System.out.println(getName() + "....aaaaaaaaaaaaaaa");
				}
			}
		}.start();
		
		new Thread() {
			public void run() {
				for(int i = 0; i < 10; i++) {
					try {
						Thread.sleep(1000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					System.out.println(getName() + "....bb");
				}
			}
		}.start();
	}

	public static void demo_sleepInMainThread() throws InterruptedException {
		for(int i = 20; i >= 0; i--) {
			Thread.sleep(1000);
			System.out.println("倒计时第" + i + "秒");
		}
	}
}

 


守护线程

setDaemon(),设置一个线程为守护线程,该线程不会单独执行,当其他非守护线程都执行结束后,自动退出。

演示:

public class Demo4_Daemon {
	public static void main(String[] args) {
		Thread t1 = new Thread() {
			public void run() {
				for(int i = 0; i < 2; i++) {
					System.out.println(getName() + "....aaaaaaaaaaaaaaaaaaaaa");
				}
			}
		};
		
		Thread t2 = new Thread() {
			public void run() {
				for(int i = 0; i < 50; i++) {
					System.out.println(getName() + "....bb");
				}
			}
		};
		
		//将t2设置为守护线程,参数传true即可。非守护线程执行完挂掉了,守护线程也就随之挂掉了,但有个时间缓冲,还能执行个几次。
		t2.setDaemon(true);
		t1.start();
		t2.start();
	}
}


加入线程

join(),当前线程暂停,等待指定的线程执行结束后,当前线程再继续。

join(int),可以等待指定的毫秒之后继续。

演示:

public class Demo5_Join {
	public static void main(String[] args) {
		final Thread t1 = new Thread() {
			public void run() {
				for(int i = 0; i < 10; i++) {
					System.out.println(getName() + "....aaaaaaaaaaaaaaa");
				}
			}
		};
		
		Thread t2 = new Thread() {
			public void run() {
				for(int i = 0; i < 10; i++) {
					if(i == 2) {
						try {
//							t1.join();	//匿名内部类在使用他所在方法中的局部变量的时候,局部变量必须用final修饰
							//join的重载方法,参数可以传毫秒值或纳秒值。插队指定的时间,过了指定的时间后,两条线程交替执行
							t1.join(1);	//t1插队1毫秒,然后恢复交替执行
						} catch (InterruptedException e) {	//抓中断异常
							
							e.printStackTrace();
						}
					}
					System.out.println(getName() + "....bb");
				}
			}
		};
		
		t1.start();
		t2.start();
	}
}


礼让线程

yield 让出cpu。

演示:

public class Demo6_Yield {
	public static void main(String[] args) {
		new MyThread().start();
		new MyThread().start();
	}
}

class MyThread extends Thread {
	public void run() {
		for(int i = 1; i <= 1000; i++) {
			if(i % 10 == 0) {
				Thread.yield();		//让出CPU
			}
			System.out.println(getName() + "...." + i);
		}
	}
}

设置线程的优先级

setPriority() 设置线程的优先级。

演示:

public class Demo7_Priority {
	public static void main(String[] args) {
		Thread t1 = new Thread() {
			public void run() {
				for(int i = 0; i < 100; i++) {
					System.out.println(getName() + "....aaaaaaaaaaaa");
				}
			}
		};
		
		Thread t2 = new Thread() {
			public void run() {
				for(int i = 0; i < 100; i++) {
					System.out.println(getName() + "....bb");
				}
			}
		};
		
		//源码
/*		public final static int MIN_PRIORITY = 1;	//线程最小的优先级是1
		public final static int NORM_PRIORITY = 5;	//线程默认的优先级是5
		public final static int MAX_PRIORITY = 10;	//线程最大的优先级是10
*/		
//		t1.setPriority(10);
//		t2.setPriority(1);
		
		t1.setPriority(Thread.MIN_PRIORITY);		//设置最小的线程优先级
		t2.setPriority(Thread.MAX_PRIORITY);		//设置最大的线程优先级
		
		t1.start();
		t2.start();
	}
}

同步代码块

什么情况下需要同步

当多线程并发,有多段代码同时执行时,我们希望某一段代码执行的过程中CPU不要切换到其他线程工作,这时就需要同步。

如果两段代码是同步的,那么同一时间只能执行一段代码。在一段代码没执行结束之前,不会执行另外一段代码。

同步代码块

使用synchronized关键字加上一个锁对象来定义一段代码,这就叫同步代码块。

多个同步代码块如果使用相同的锁对象,那么他们就是同步的。

演示:

public class Demo1_Synchronized {
	public static void main(String[] args) {
		final Printer p = new Printer();
		
		new Thread() {
			public void run() {
				while(true) {
					p.print1();
				}
			}
		}.start();
		
		new Thread() {
			public void run() {
				while(true) {
					p.print2();
				}
			}
		}.start();
	}
}

class Printer {
	Demo d = new Demo();
	public void print1() {
		synchronized(d/*new Demo()*/) {		//同步代码块,锁机制,锁对象可以是任意的
			System.out.print("H");
			System.out.print("e");
			System.out.print("l");
			System.out.print("l");
			System.out.print("o");
			System.out.print("!");
			System.out.print("\r\n");
		}
	}
	
	public void print2() {
		synchronized(d/*new Demo()*/) {		//锁对象不能用匿名对象new Demo(),因为匿名对象不是同一个对象
			System.out.print("W");
			System.out.print("o");
			System.out.print("r");
			System.out.print("l");
			System.out.print("d");
			System.out.print("!");
			System.out.print("\r\n");
		}
	}
}

class Demo {}

同步方法

使用synchronized关键字修饰一个方法,该方法中所有的代码都是同步的。

演示:

public class Demo2_Synchronized {
	public static void main(String[] args) {
		final Printer2 p2 = new Printer2();
		final Printer3 p3 = new Printer3();
		//非静态同步方法
		new Thread() {
			public void run() {
				while(true) {
					p2.print1();
				}
			}
		}.start();
		new Thread() {
			public void run() {
				while(true) {
					p2.print2();
				}
			}
		}.start();
		
		//静态同步方法
/*		new Thread() {
			public void run() {
				while(true) {
					p3.print1();
				}
			}
		}.start();
		new Thread() {
			public void run() {
				while(true) {
					p3.print2();
				}
			}
		}.start();*/
	}
}

class Printer2 {
	//非静态的同步方法的锁对象是this。
	public synchronized void print1() {		//同步方法只需要在方法上加synchronized关键字即可
		System.out.print("H");
		System.out.print("e");
		System.out.print("l");
		System.out.print("l");
		System.out.print("o");
		System.out.print("!");
		System.out.print("\r\n");
	}
	
	public void print2() {
		synchronized(this) {	
			System.out.print("W");
			System.out.print("o");
			System.out.print("r");
			System.out.print("l");
			System.out.print("d");
			System.out.print("!");
			System.out.print("\r\n");
		}
	}
}

class Printer3 {
	//静态的同步方法的锁对象是该类的字节码对象
	public static synchronized void print1() {		//静态是优先于对象存在的,静态方法中是不能定义this和super的,this在创建对象时才有值 
		System.out.print("H");
		System.out.print("e");
		System.out.print("l");
		System.out.print("l");
		System.out.print("o");
		System.out.print("!");
		System.out.print("\r\n");
	}
	
	public void print2() {
		synchronized(Printer3.class) {		//类加载时,有字节码对象  所以可以是字节码对象也可以是在外面声明的一个静态的对象
			System.out.print("W");
			System.out.print("o");
			System.out.print("r");
			System.out.print("l");
			System.out.print("d");
			System.out.print("!");
			System.out.print("\r\n");
		}
	}
}

线程安全问题

多线程并发操作同一数据时,就有可能出现线程安全问题。

使用同步技术可以解决这种问题,把操作数据的代码进行同步,不要多个线程一起操作。

演示:

/**
 *  需求:铁路售票,一共100张,通过四个窗口卖完.
 */
public class Demo3_Ticket {
	public static void main(String[] args) {
		//7.开启四条线程  
		new Ticket().start();
		new Ticket().start();
		new Ticket().start();
		new Ticket().start();
	}
}
//1.定义Ticket类去继承Thread
class Ticket extends Thread {
	//2.定义一个成员变量表示票的数量
//	private int ticket = 100;	问题1:每个对象里都有ticket这个成员变量,结果导致四人各卖各的100张,共卖出去400张而不是公共的100张。
	private static int ticket = 100;	//问题1解决方法:给ticket成员变量加静态。
	private static Object obj = new Object();	//如果用引用数据类型成员变量当作锁对象,必须是静态的。
	//3.重写run()
	public void run() {
		//4.定义一个无限循环,表示票不断在买
		while(true) {
			synchronized(Ticket.class/*obj*/) { /*this做参数不行,this代表new ticket,但是new ticket有四个,每个线程都有自己的对象,每new一次都是一个不同的对象
				Object类对象作参数也不行,private Object obj = new Object();相当于一个成员变量,每个对象都有自己的成员变量。如果非要用obj当锁对象就加静态,加了静态后相当于四个对象共享了一个锁对象*/
				//5.判断当票的数量等于0时,跳出循环,不卖啦
				if(ticket == 0) {		//问题2解决方法:我们需要将while中的代码块进行同步。当多线程并发想改变同一个数据的时候,建议用同步代码块。
					break;
				}
				
				try {
					Thread.sleep(10);		/*通过睡眠模拟n多行代码在执行  出现的问题2是最后不断的卖出负数票  可能情况是,只剩1张票时四个线程顺序进入程序通过判断不为0,然后先后休眠。
				当先睡的醒了往下执行,输出他为第1号票,ticket减一变成0.接着第二个线程醒来,输出他为第0号票,ticket减一变成-1,之后第三、第四个线程输出-1、-2号票。
				当线程再进程序进行判断时,从-3开始往下减都不为0,相当于跨过了等于0这一步,就不断卖起了负数票。就算判断条件改为<=0,也会出现卖负数票(0号票,-1号票之类)的情况,具体视最后休眠的线程数而定*/
				} catch (InterruptedException e) {
					
					e.printStackTrace();
				}
				
				//6.打印一下看是谁卖的票
				System.out.println(getName() + "...这是第" + ticket-- + "号票");
			}
		}
	}
}

火车站卖票的例子用实现Runnable接口

演示:

/**
 * 火车站卖票的例子用实现Runnable接口
 */
public class Demo4_Ticket {
	public static void main(String[] args) {
		MyTicket mt = new MyTicket();
		new Thread(mt).start();		//将MyTicket当作资源传给Thread的构造函数即可。因为创建对象只创建了一次,100张票只有一次
		new Thread(mt).start();
		new Thread(mt).start();
		new Thread(mt).start();
		
/*		Thread t1 = new Thread(mt);		//IllegalThreadStateException 多次启动一个线程是非法的。特别是当线程已经结束执行后,不能再重新启动。
		t1.start();
		t1.start();
		t1.start();
		t1.start();*/
	}
}

class MyTicket implements Runnable {
	private int tickets = 100;		//只需要创建一个对象就可以了,所以不需要加静态
	public void run() {
		while(true) {
			synchronized(MyTicket.class) {		//锁对象也可以用this,this即mt
				if(tickets <= 0) {
					break;
				}
				
				try {
					Thread.sleep(10);
				}catch (InterruptedException e) {
					
					e.printStackTrace();
				}
				
				System.out.println(Thread.currentThread().getName() + "...这是第" + tickets-- + "号票");
			}
		}
	}
}

死锁

多线程同步的时候,如果同步代码嵌套,使用相同锁,就有可能出现死锁。所以尽量不要嵌套使用。

演示:

public class Demo5_DeadLock {
	private static String s1 = "筷子左";
	private static String s2 = "筷子右";
	
	public static void main(String[] args) {
		/*同步代码块的嵌套使用.线程1获取s1启动,在拿到筷子左等待筷子右时,线程2获取s2启动,拿到筷子右等待筷子左。线程二想获取s1拿到筷子左,但s1在线程1手里并没有被释放,因为s1只有在执行到对应的同步代码块执行结束后才会释放。线程1想获取s2拿到筷子右,同理线程2也没有释放s2。
		线程二想拿s1拿不到,线程1想拿s2拿不到。两条线程僵持在这里,程序锁住了。这就是所谓的死锁*/
		new Thread() {
			public void run() {
				while(true) {
					synchronized(s1) {
						System.out.println(getName() + "...获取" + s1 + "等待" + s2);
						synchronized(s2) {
							System.out.println(getName() + "...拿到" + s2 + "开吃");
						}
					}
				}
			}
		}.start();
		
		new Thread() {
			public void run() {
				while(true) {
					synchronized(s2) {
						System.out.println(getName() + "...获取" + s2 + "等待" + s1);
						synchronized(s1) {
							System.out.println(getName() + "...拿到" + s1 + "开吃");
						}
					}
				}
			}
		}.start();
	}
}

以前的线程安全的类回顾

回顾以前说过的线程安全问题

看源码:Vector,StringBuffer,Hashtable,Collections.synchroinzed(xxx)

Vector

public synchronized void addElement(E obj) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = obj;
}

ArrayList

public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
}

StringBuffer

public synchronized StringBuffer append(Object obj) {
        toStringCache = null;
        super.append(String.valueOf(obj));
        return this;
}

StringBuilder

public StringBuilder append(Object obj) {
        return append(String.valueOf(obj));
}

Hashtable

public synchronized V put(K key, V value) {
        // Make sure the value is not null
        if (value == null) {
            throw new NullPointerException();
        }

        // Makes sure the key is not already in the hashtable.
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }

        addEntry(hash, key, value, index);
        return null;
}

HashMap

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
}

Vector是线程安全的,ArrayList是线程不安全的。

StringBuffer是线程安全的,StringBuilder是线程不安全的。

Hashtable是线程安全的,HashMap是线程不安全的。

Collections.synchroinzed(xxx)可以将线程不安全的线程变成线程安全的。

Collections类概述

public class Collections extends Object,此类完全由在collection上进行操作或返回collection的静态方法组成。

Collections类的成员方法

public static <T> List<T> synchronizedList(List<T> list) 返回指定列表支持的同步(线程安全的)列表。那么我们就可以淘汰Vector了,ArrayList如果不需要同步,我们就直接用ArrayList,如果你需要同步,我们就调用这个方法将ArrayList同步。

public static <T> Collection<T> synchronizedCollectionCollection<T> c) 返回指定Collection支持的同步(线程安全的)Collection。

public static <K, V> List<K, V> synchronizedMap(Map<T> m) 返回指定映射支持的同步(线程安全的)映射。

public static <T> Set<T> synchronizedSet(Set<T> m) 返回指定Set支持的同步(线程安全的)Set。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值