1.线程相关概念
2.多线程的实现方式
3.主线程和线程常见方法
4.线程的生命周期
5.线程安全问题
6.线程协作
7.线程相关类 ThreadLocal
8.线程池
9.单例设计模式
10.垃圾回收
11.枚举类型
一、线程相关概念
1.程序和进程
程序(program):一个固定的运行逻辑和数据的集合,是一个静态的状态,一般存储在硬盘中。
进程(process):一个正在运行的程序,是一个程序的一次运行,是一个动态的概念,一般存储在内存中。有自己的地址空间。
“多任务”:指操作系统能同时运行多个进程(程序)。如window7系统可以同时运行写字板程序、画图程序、WORD、EXCEL等。
一般来说,一个应用对应一个进程,进程是程序运行的载体。
2.进程和线程
进程(process):一个正在执行的程序,有自己独立的资源分配,是一个独立的资源分配单位。——独占内存
线程(thread):是进程内部单一的一个顺序控制流,一条独立的执行路径。多线程,在执行某个程序的时候,该程序可以有多个子任务,每个线程都可以独立的完成其中一个任务。在各个子任务之间,没有什么依赖关系,可以单独执行。
进程和线程的关系:
一个进程中,可以有多条线程;但是一个进程中,至少有一条线程。
线程不会独立的分配资源,一个进程中的所有线程,共享同一个进程中的资源。
3.并行和并发
并行(parallel):多个任务(进程、线程)同时运行。在某个确定的时刻,有多个任务在执行。
条件:有多个cpu,多核编程。
并发(concurrent):多个任务(进程、线程)同时发起。不能同时执行的(只有一个cpu),只能是同时要求执行。就只能在某个时间片内,将多个任务都有过执行。一个cpu在不同的任务之间,来回切换,只不过每个任务耗费的时间比较短,cpu的切换速度比较快,所以可以让用户感觉就像多个任务在同时执行。
并发的本质:不同任务来回切换
既然只有一个CPU,还需要在不同的任务之间来回切换,那么效率到是提升了还是降低了?
提升了:整个系统的效率提升了(cpu使用率提升了),对于某个任务而言,效率是效低了。
并行和并发的比较
二、多线程的实现方式
1.Java中的多线程
大部分时候,我们都做的是单线程编程。前面所有程序都只有一条顺序执行流,程序从main方法开始执行依次执行每行代码。一旦出错程序将停止。
但是实际上,单线程的程序功能非常有限,比如要开发一个服务器程序,如果是单线程,那么就只能为一个用户提供服务,而我们想要的是向多个用户同时服务而互不干扰。
单线程就好像只雇了一个服务员的餐馆,只能干完这样再干那样。
Java语言内置多线程功能支持,而不是单纯的调度底层的操作系统。
2.多线程的第一种实现方式:继承方式
步骤:
1 定义一个类,继承Thread类
2 重写Thread类的run方法
3 创建对象
4 调用启动线程的方法(start方法)
注意事项
多线程执行的结果有随机性
线程对象在start()执行,执行run()方法的任务,开启一个子线程
线程对象调用run方法,不会产生随机结果,因为一个普通对象在调用普通方法,默认顺序执行
代码示例
public class Test {
public static void main(String[] args){
myThread mt = new myThread();
mt.start();//开启一个线程
for(int i=0;i<=10;i++){
System.out.println("main...."+i);
}//主方法也是一个线程
mt.run();//普通对象在调用普通方法,默认顺序执行,并没有开启一个新的线程
}
}
class myThread extends Thread{
public void run(){
for(int i=0;i<=10;i++){
System.out.println("myThread..."+i);
}
}
}
练习:火车站售票
public class Test {
public static void main(String[] args){
saleWindow sw1 = new saleWindow("第一个窗口");
saleWindow sw2 = new saleWindow("第二个窗口");
saleWindow sw3 = new saleWindow("第三个窗口");
sw1.start();
sw2.start();
sw3.start();
}
}
class saleWindow extends Thread{
static int ticket = 20;
public saleWindow(){};
public saleWindow(String name){
super(name);
}
public void run(){
while(true){
if(ticket>0){
System.out.println(Thread.currentThread().getName()+"卖出了第"+ticket--+"张票");
}else{
break;
}
}
}
}
3.多线程的第二种实现方式:实现接口方式
步骤:
1、定义一个任务类,实现Runnable接口
2、重写任务类中的run方法,用于定义任务的内容
3、创建任务类对象,表示任务
4、创建一个Thread类型的对象,用于执行任务类对象
5、调用线程对象的start方法,开启新线程
代码示例
public class Test {
public static void main(String[] args){
MyTask mt = new MyTask();
Thread t1 = new Thread(mt,"子线程1");
Thread t2 = new Thread(mt,"子线程2");
t1.start();
t2.start();
}
}
class MyTask implements Runnable{
public void run(){
for(int i=0;i<=10;i++){
System.out.println(Thread.currentThread().getName()+"..."+i);
}
}
}
4.两种方式的比较
1.代码复杂度
继承Thread方式比较简单
实现Runnable接口的方式比较复杂
2.实现原理
继承方式:调用start()方法,内部调用start0()方法,因为start0()是一个本地方法(C语言),虚拟机实现,所以在java中看不到源代码,本地方法返回来的java中的run方法,run方法已经被子类给重写,所以最终运行的是子类重写的run方法
实现方式:构造方法中,将Runnable的实现类对象传入给线程对象,经过一种init()初始化,最终,用于给Thread类的某个成员变量赋值,调用start()方法,最终也是返回来调用Thread中的run()方法,判断当前的target 是否为空,如果为空,出现异常,如果不为null,调用run()方法,因为Runnable接口的run方法已经被重写,最终调用是我们重写过的run方法。
3.设计
继承方式:某个类继承Thread类,那么就无法继承其它的类,就限制了扩展,所以扩展性比较差。
实现方式:某个类实现Runnable接口,传入线程类中,这个实现类不但可以表示线程任务,同时还能继承别的逻辑相关的类。
4.灵活性
继承方式:将线程对象和任务内容绑定在了一起,耦合性较强,灵活性较弱。
实现方式:完全把任务和线程对象分离开来,灵活性较高
5.使用匿名内部类创建线程对象
代码示例
public class Test {
public static void main(String[] args){
//第一种匿名内部类实现多线程
new Thread(){
public void run(){
for(int i=0;i<=10;i++){
System.out.println(Thread.currentThread().getName()+".."+i);
}
}
}.start();
//第二种匿名内部类实现多线程
new Thread(new Runnable(){
public void run(){
for(int i=0;i<=10;i++){
System.out.println(Thread.currentThread().getName()+"..."+i);
}
}
}).start();
}
}
三、主线程和线程常见方法
1.主线程
在任何Java程序启动时,一个线程立刻运行(即main方法对应的线程),该线程通常称为程序的主线程。
主线程的特点:
它是产生其他子线程的线程。
它不一定是最后完成执行的线程,子线程可能在它结束之后还在运行。
代码示例
public class Test {
public static void main(String[] args){
//子线程1
new A().start();
//子线程2
new B().start();
//主线程
for(int i=1;i<=20;i++){
System.out.println(Thread.currentThread().getName()+"..."+i);
}
}
}
class A extends Thread{
public void run(){
for(int i =0;i<=20;i++){
System.out.println(Thread.currentThread().getName()+"..."+i);
}
}
}
class B extends Thread{
public void run() {
for(int i=1;i<=20;i++){
System.out.println(Thread.currentThread().getName()+"..."+i);
}
}
}
2.常见方法
构造方法
Thread(String name) 分配一个新的 Thread对象。 |
Thread(Runnable target, String name) 分配一个新的 Thread对象。 |
常见方法
String | getName() 返回此线程的名称。 |
注意事项:
如果没有线程命名,那么线程的默认名称 Thread-x x 从0开始
Runnable实现类中,没有getName()方法
void | setName(String name) 将此线程的名称更改为等于参数 name 。 |
线程设置名字,既可以在启动之前设置,也可以启动之后设置
static Thread | 返回对当前正在执行的线程对象的引用。 |
作用:某段代码只要在执行,就一定在某个方法中执行,只要在某个方法中,代码就一定是被某个线程执行。所以任意一句代码,都有执行这句代码的线程对象。
哪个线程在执行这段代码,返回的就是哪个线程对象。
代码示例
public class Test {
public static void main(String[] args){
Demo d1 = new Demo();
Task t2 = new Task();
Thread t = new Thread(t2,"子线程2");
Demo d3 = new Demo();
d1.start();
t.start();
d3.start();
d3.setName("子线程3");
}
}
class Demo extends Thread{
public void run(){
System.out.println(this.getName());
}
}
class Task implements Runnable{
public void run(){
System.out.println(Thread.currentThread().getName());//Thread.currentThread() 获得当前正在运行的线程对象
}
}
void | setPriority(int newPriority) 更改此线程的优先级。 |
static int | 线程可以拥有的最大优先级。 10 |
static int | 线程可以拥有的最小优先级。 1 |
static int | 分配给线程的默认优先级。 5 |
通过某些方法,设定当前的线程的优先级,优先级高的线程先运行,优先级低的线程后运行,优先级越高的线程抢到CPU执行权概率更大一些,有可能优先级高的线程先执行完毕。但并不是有高优先级的线程时,低优先的线程就不能执行。
数字范围:最小1,最大10,默认状态就是5。
代码示例
public class Test {
public static void main(String[] args){
Thread t1 = new Thread(){
public void run(){
for(int i=0;i<=10;i++){
System.out.println("MAX:"+i);
}
}
};
t1.setPriority(Thread.MAX_PRIORITY);
Thread t2 = new Thread(){
public void run(){
for(int i=0;i<=10;i++){
System.out.println("MIN:"+i);
}
}
};
t2.setPriority(Thread.MIN_PRIORITY);
Thread t3 = new Thread(){
public void run(){
for(int i=0;i<=10;i++){
System.out.println("NORM:"+i);
}
}
};
t3.setPriority(Thread.NORM_PRIORITY);
t1.start();
t2.start();
t3.start();
}
}
四、线程的生命周期
1.概述
线程是一个动态的概念,有创建的时候,也有运行和变化的时候,必须也有消亡的时候,所以从生到死就是一个生命周期。在生命周期中,有各种各样的状态,这些状态可以相互转换。
新建态:刚创建好对象的时候,刚new出来的时候。
就绪态:线程准备好了所有运行的资源,只差cpu来临。
运行态:cpu正在执行的线程的状态。
阻塞态:线程主动休息、或者缺少一些运行的资源,即使cpu来临,也无法运行。
死亡态:线程运行完成、出现异常、调用方法结束。
2.Java中关于线程状态的描述
Thread.State | getState() 返回此线程的状态。 |
说明:由于状态是有限个的,所以该类的对象,不需要手动创建,直接在类中创建好了,获取即可,这种类型就是枚举类型。每个对象,称为枚举项。Thread.State中,枚举项:6个。
NEW:新建态,没有开启线程
RUNNABLE:就绪态和运行态
BLOCKED:阻塞态(等待锁、I\O)
WAITING:阻塞态(调用了wait方法,等待其他线程唤醒的状态)
TIMED_WAITING:阻塞态(调用了有时间限制的wait方法、sleep方法)
TERMINATED:死亡态
代码示例
public class Test {
public static void main(String[] args) {
Thread t1 = new Thread(){
public void run(){
synchronized("abc"){
for(int i=0;i<5;i++){
System.out.println(i+"..."+getName());
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
};
Thread t2 = new Thread(){
public void run(){
synchronized("abc"){
for(int i=0;i<5;i++){
System.out.println(i+"..."+getName());
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
};
t1.start();
t2.start();
while(true){
System.out.println(t1.getState()+".....t1");
System.out.println(t2.getState()+".....t2");
System.out.println("------------------------------");
try{
Thread.sleep(1000);
}catch(InterruptedException e){
e.printStackTrace();
}
if(t1.getState().equals(Thread.State.TERMINATED) && t2.getState().equals(Thread.State.TERMINATED)){
System.out.println("两条线程都结束了。。。");
break;
}
}
}
}
3.线程的睡眠方式
static void | sleep(long millis) 导致正在执行的线程以指定的毫秒数加上指定的纳秒数来暂停(临时停止执行),这取决于系统定时器和调度器的精度和准确性。 |
代码示例
public class Test {
public static void main(String[] args){
A a = new A();
a.start();
}
}
class A extends Thread{
public void run(){
for(int i=1;i<=50;i++){
System.out.println("A..."+i);
if(i==10){
try{
sleep(3000);//当i==10时,程序会停下一段时间再运行
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
}
InterruptedException:中断异常
在普通方法,可以声明
在run方法中,必须只能处理,不能声明
4.停止线程的方式
(1)线程的stop方法(过时)
void | stop() 已弃用 这种方法本质上是不安全的。 使用Thread.stop停止线程可以解锁所有已锁定的监视器(由于未ThreadDeath ThreadDeath异常在堆栈中ThreadDeath的自然结果)。 如果先前受这些监视器保护的任何对象处于不一致的状态,则损坏的对象将变得对其他线程可见,可能导致任意行为。 stop许多用途应该被替换为只是修改一些变量以指示目标线程应该停止运行的代码。 目标线程应该定期检查此变量,如果变量表示要停止运行,则以有序方式从其运行方法返回。 如果目标线程长时间等待(例如,在interrupt变量上),则应该使用interrupt方法来中断等待。 有关详细信息,请参阅Why are Thread.stop, Thread.suspend and Thread.resume Deprecated? 。 |
示例代码
public class Test {
public static void main(String[] args){
A a = new A();
a.start();
for(int i=1;i<=20;i++){
System.out.println("main..."+i);
if(i==10){
a.stop();//停止子线程
}
try{
sleep(300);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
class A extends Thread{
public void run(){
for(int i=1;i<=30;i++){
System.out.println("A..."+i);
try{
sleep(300);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
(2)定义标识的方法
代码示例:主线程循环到10的时候,停止子线程。
public class Test {
public static boolean flag = false;
public static void main(String[] args){
A a = new A();
a.start();
for(int i=1;i<=20;i++){
System.out.println("main..."+i);
if(i==10){
flag = true;
}
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class A extends Thread{
public void run(){
for(int i=1;i<=30;i++){
System.out.println("A..."+i);
if(Test.flag==true){
break;
}
try{
sleep(300);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
5.线程的join方法
void | join(long millis) 等待这个线程死亡最多 millis毫秒。 |
void | join(long millis, int nanos) 等待最多 millis毫秒加上 nanos纳秒这个线程死亡。 |
非静态方法,当前线程挂起(阻塞)等待加入(join)的线程执行完成。
当前线程调用t.join()意味着,当前线程挂起直到目标线程t结束。
示例代码
public class Test {
public static void main(String[] args){
Thread t1 = new Thread(new X());
for(int i=1;i<=10;i++){
System.out.println("main..."+i);
if(i==5){
t1.start();
try{
t1.join();//当线程的i==10时,t1线程加入
//主线程挂起(暂停),当t1线程运行结束后,主线程继续
//t1.join(500);//指定加入多少毫秒
}catch(InterruptedException e){
e.printStackTrace();
}
try{
Thread.sleep(100);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
}
class X implements Runnable{
public void run(){
for(int i=0;i<=10;i++){
System.out.println("X..."+i);
try{
Thread.sleep(100);
}catch(InterruptedException e){
e.printStackTrace();
}
}
}
}
6.线程的yield方法
static void | yield() 对调度程序的一个暗示,即当前线程愿意产生当前使用的处理器。 |
和sleep方法很像,但是和sleep不同。它只是短暂地挂起当前线程,让别的线程先运行,而自己进入到就绪状态。
在大多数实现中,线程只“让步”于优先级相同或更高的线程。
代码示例
public class Test {
public static void main(String[] args){
Y y = new Y();
y.setPriority(Thread.MAX_PRIORITY);
y.start();
for(int i=1;i<=15;i++){
System.out.println("main..."+i);
if(i%2==0){
Thread.yield();
}
}
}
}
class Y extends Thread{
public void run(){
for(int i=1;i<=15;i++){
System.out.println("Y..."+i);
if(i%2==0){
yield();
}
}
}
}
7.守护线程(后台线程)
当所有线程完成,Java程序退出,这句话并不准确,因为gc等JVM线程,我们无法停止他们,为何Java程序还是会退出?
这些“系统线程”被称为“守护线程”,Java程序在所有非守护线程完成后退出。这些线程只有在别的线程运行的时候才有意义。
void | setDaemon(boolean on) 将此线程标记为 daemon线程或用户线程。 |
每条线程默认不是守护线程,只有设置了flag为true之后,该线程才变成守护线程。
特点:守护线程就是用于守护其它线程可以正常运行的线程,在为其它的核心线程良好的环境,如果非守护线程全部死亡,守护线程就没有存在必要了,一段时间之后,也一同结束。
别名:后台线程
注意:必须在启动线程之前(调用start方法之前)调用setDaemon(true)方法,才可以把该线程设置为后台线程。
代码示例
public class Test {
public static void main(String[] args){
Thread t1 = new Thread(){
public void run(){
for(int i=0;i<=10;i++){
System.out.println("Thread...."+i);
}
}
};
t1.setDaemon(true);//守护线程的for循环并没有执行完
t1.start();
//主线程
for(int i=0;i<=5;i++){
System.out.println("main..."+i);
}
}
}
五、线程安全问题
1、线程共享数据
某些代码在没有执行完成的时候,cpu就可能被其它的线程抢走,结果导致当前代码中的一些据发生错误。
前面我们看到的多线程都是独立的,但在真正的应用中,多个线程必须以某种方式通讯或者说共享数据。
在这种情况下,我们必须使用“同步机制”来确保值被正确地传递,并防止“数据不一致”。
2.线程同步(锁机制)
以上线程共享数据的问题,如何解决?
Java中可用synchronized关键字确保数据在多个线程间正确地共享。
某段代码执行的时候,cpu不要切换影响到当前代码的执行,可以确保cpu在执行A线程的时候,哪怕别的线程抢到CPU执行权,也不会影响A线程执行的代码。
格式:
synchronized(锁对象){
需要保证完整性、原子性的一段代码(同步代码块)
}
两层含义:
同一时间只有一个线程执行“保护区”的代码,我们称之为互斥(mutex)。确保被某一线程更改的值对别的线程可见。
解决方式:
操作的时候加锁,操作结束解锁。
撞到锁的一方等待,解锁时唤醒等待一方。
如何加锁? 使用synchronized关键字。
Object的继承类都可充当“锁”角色
synchronized(Object){ 需要排斥的操作; }
注意:要实现互斥的代码,必须使用同一个锁。
代码示例
public class Test {
private int n;
class A implements Runnable{
public void run() {
//synchronized("aaa"){
synchronized(this){
while(n<=5){
System.out.println("A..."+n);
n++;
}
}
}
}
class B implements Runnable{
public void run() {
//synchronized("aaa"){
synchronized(this){
while(n<10){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("B..."+n);
n++;
}
}
}
}
public static void main(String[] args) {
/*Test.A a = new Test().new A();
Test.B b = new Test().new B();*/
Test test = new Test();
Test.A a = test.new A();
Test.B b = test.new B();
Thread t1 = new Thread(a);
Thread t2 = new Thread(b);
t1.start();
t2.start();
}
}
使用同步代码的之后的效果:
当cpu想去执行同步代码块的时候,需要先获取锁对象,获取之后就可以运行代码块中的内容;当cpu正在执当前代码的内容时,cpu也可以切换到其它的代码,但是不能切换到具有【相同锁对象】的代码上。
当cpu执行完当前代码中的内容之后,就会释放锁对象,cpu可以运行其它具有当前锁对象的同步代码块了。
线程对象必须共同使用的是同一个锁对象,锁对象可以是任意对象。
3.同步方法(线程安全的类)
在某段代码执行的时候,不希望cpu切换到其他影响当前线程的线程上去,就在这段代码上加上同步代码块。
如果某个方法中,所有的代码都需要加上同步代码块,使用同步方法这种【简写格式】来替代同步代码块。
格式
权限修饰符 [静态修饰符] synchronized 返回值类型 方法名称(参数列表) {
需要同步的方法体
}
同步方法的锁对象
如果是非静态的方法,同步方法的锁对象就是this,当前对象,哪个对象调用这个同步方法,这个同步方法使用的锁就是哪个对象。
如果是静态的方法,同步方法的锁对象就是当前类的字节码对象,类名.class(在方法区的一个对象),哪个类在调用这个同步方法,这个同步方法使用的锁就是哪个类的字节码对象。
示例代码
public class Test {
public static void main(String[] args) {
Printer printer = new Printer();
new Thread(){
public void run(){
while(true){
try{
Thread.sleep(200);
}catch(InterruptedException e){
e.printStackTrace();
}
printer.print1();
}
}
}.start();
new Thread() {
public void run() {
while(true) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
printer.print2();
}
}
}.start();
}
}
class Printer {
public static synchronized void print1(){
//synchronized(this){
System.out.println("11");
System.out.println("22");
System.out.println("33");
System.out.println("44");
//}
}
public void print2(){
synchronized (Printer.class){
System.out.println("a");
System.out.println("b");
System.out.println("c");
}
}
}
4.死锁
锁对象的说明:
线程加上同步是为了让两条线程相互不影响,如果相互影响,影响的就是数据的正确性。如果两条使用锁对象,其实主要是为了锁数据,当某条线程在操作这个数据时,其他的线程不能操作当前的数据。线程要操作相同的数据,那就这两条线程需要在操作数据的部分加上同步代码块,并使用相同的锁对象。
多个线程竞争多个排他锁的时候,可能出现死锁。
互相等待对方资源,称之为死锁。
最典型的死锁:线程1持有ObjectA上的锁,并等待ObjectB上的锁,线程2持有ObjectB上的锁,并等待ObjectA上的锁,每个线程都不放弃获得的第一个锁,并一直等待第二个锁,因此会永远等下去。
代码示例
public class Test {
public static void main(String[] args) {
new Thread("张三"){
public void run(){
while(true){
synchronized("筷子A"){
System.out.println("张三拿到了筷子A,等待筷子B");
synchronized("筷子B"){
System.out.println("张三拿到了筷子A和B");
}
}
}
}
}.start();
new Thread("李四"){
public void run(){
while(true){
synchronized("筷子B"){
System.out.println("李四拿到了筷子B,等待筷子A");
synchronized("筷子A"){
System.out.println("李四拿到了筷子A和B");
}
}
}
}
}.start();
}
}
六、线程协作
我们已了解线程之间可能的冲突,以及怎样避免冲突;下一步就要学习怎样使线程之间相互协作。
这种协作是通过线程之间的握手来实现的,这种握手可以通过Object的wait()和notify()来安全的实现。
1.生产者和消费者问题
典型问题:生产者&消费者问题
有一家汉堡店举办吃汉堡比赛,决赛时有3个顾客来吃,3个厨师来做,一个服务员负责协调汉堡的数量。为了避免浪费,制作好的汉堡被放进一个能装有10个汉堡的长条状容器中,按照先进先出的原则取汉堡。如果容器被装满,则厨师停止做汉堡,如果顾客发现容器内的汉堡吃完了,就可以拍响容器上的闹铃,提醒厨师再做几个汉堡出来。此时服务员过来安抚顾客,让他等待。而一旦厨师的汉堡做出来,就会让服务员通知顾客,汉堡做好了,让顾客继续过来取汉堡。
顾客其实就是我们所说的消费者,而厨师就是生产者。
import java.util.List;
import java.util.ArrayList;
public class Test {
public static void main(String[] args) {
new Producer().start();
new Consumer().start();
}
private static List<Integer> hamburgers = new ArrayList<Integer>();
private static class Producer extends Thread{ //生产者
int i=0;
public void run() {
while(true){
if(hamburgers.size()<10){
hamburgers.add(i); // 生产
System.out.println("生产汉堡: "+i);
i++;
synchronized (hamburgers) {
hamburgers.notifyAll();
}
}else{
synchronized (hamburgers) {
try {
hamburgers.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
private static class Consumer extends Thread{//消费者
public void run() {
while(true){
if(hamburgers.size()>0){
System.out.println("消费汉堡: "+hamburgers.remove(0)); // 消费
synchronized (hamburgers) {
hamburgers.notifyAll();
}
}else{
synchronized (hamburgers) {
try {
hamburgers.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
2.线程协作案例
需求:线程 t1 循环输出数字 主线程控制t1的暂停和继续。
主线程使用键盘输入 输入一次 t1 暂停 输入两次 t1继续 三次...
练习一:使用synchronzied 完成线程的协作
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class Test {
public static void main(String[] args) throws Exception{ // 主线程
T t = new T();
t.start();
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
while(true){
br.readLine(); // 主线程阻塞,等待键盘输入
synchronized("abc"){ // 键盘第一次输入后,主线程立刻抢占了 "abc" 锁
T.flag = true; //再把flag变为true
br.readLine(); // 等着键盘第二次输入,才释放锁
T.flag = false;
}
}
}
}
class T extends Thread{
public static boolean flag = false;
public void run() {
for(int i=1;i<=100;i++){
if(flag){ // 在循环中一直判断flag 当主线程将flag变true时,进入if 去抢"abc"这个锁,而这时"abc"锁已经被主线程占用,只能等待
synchronized("abc"){
}
}
System.out.println("T线程..."+i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
练习二:使用wait 和notify 完成线程协作
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class Test {
public static void main(String[] args) throws Exception{
TT tt = new TT();
tt.start();
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
while(true){
br.readLine();
TT.flag = true;
br.readLine();
synchronized ("aaa") {
"aaa".notify(); // notify()表示唤醒在这个对象下等待的某一个线程
TT.flag = false;
}
}
}
}
class TT extends Thread{
public static boolean flag = false;
public void run() {
for(int i=1;i<=100;i++){
if(flag){
try {
synchronized ("aaa") { // 调用wait()和 notify()方法的代码,必须放在同步代码块中
"aaa".wait(); // 当线程中调用了 "aaa".wait()时,表示当前线程在"aaa"这个对象下等待(进入了阻塞状态——挂起)
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("TT线程###"+i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
练习三:使用notifyAll
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class Test {
public static boolean flag = false;
public static void main(String[] args) throws Exception{
AA aa = new AA();
BB bb = new BB();
aa.start();
bb.start();
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
while(true){
br.readLine();
flag = true;
br.readLine();
synchronized ("aaa") {
//"aaa".notify(); // notify()表示唤醒在这个对象下等待的某一个线程
"aaa".notifyAll();
flag = false;
}
}
}
}
class AA extends Thread{
public void run() {
for(int i=1;i<=100;i++){
if(Test.flag){
try {
synchronized ("aaa") { // 调用wait()和 notify()方法的代码,必须放在同步代码块中
"aaa".wait(); // 当线程中调用了 "aaa".wait()时,表示当前线程在"aaa"这个对象下等待(进入了阻塞状态——挂起)
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("AA线程###"+i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
class BB extends Thread{
public void run() {
for(int i=1;i<=100;i++){
if(Test.flag){
try {
synchronized ("aaa") { // 调用wait()和 notify()方法的代码,必须放在同步代码块中
"aaa".wait(); // 当线程中调用了 "aaa".wait()时,表示当前线程在"aaa"这个对象下等待(进入了阻塞状态——挂起)
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("BB线程..."+i);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
七、线程相关类 ThreadLocal
ThreadLocal是什么呢?其实ThreadLocal并非是一个线程的本地实现版本,它并不是一个Thread,而是threadlocalvariable(线程局部变量)。也许把它命名为ThreadLocalVar更加合适。线程局部变量(ThreadLocal)实际的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,就是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。
示例代码
import java.util.List;
import java.util.ArrayList;
public class Test{
private List<String> list = new ArrayList<String>();
private ThreadLocal<String> tl = new ThreadLocal<String>();
//tl.set("xxx"); // 向ThreadLocal中存值
//tl.get(); // 取出ThreadLocal中的值
/*
* 在多线程的环境中,每一个线程操作的ThreadLocal对象,都不是同一个。
*
* 而是ThreadLocal会为每一个线程提供一个副本
*
* */
public static void main(String[] args) {
Test outer = new Test();
Test.A a = outer.new A();
a.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
Test.B b = outer.new B();
b.start();
}
class A extends Thread{
public void run() {
list.add("zhangsan");
tl.set("lisi");
//System.out.println(tl.get());
}
}
class B extends Thread{
public void run() {
System.out.println(list.get(0));//zhangsan
System.out.println(tl.get());//null
}
}
}
八、线程池
1.线程池介绍
系统启动一个新线程的成本是比较高的。因为涉及到和操作系统的交互。在这种情况下使用线程池可以很好的提高性能。尤其是当程序中需要创建大量生存期很短暂的线程时,更应该考虑使用线程池。
没有线程池的状态:
当我们使用一条线程的时候,先将线程对象创建出来,启动线程,在运行过程中,可能会完成任务,也可能会在中途被任务内容中断掉,任务还没有完成。
即使是能够正常完成,线程对象就结束了,就变成了垃圾对象,需要被垃圾回收器回收。
如果在系统中,大量的任务都是小任务,任务消耗时间较短、线程对象的创建和消亡耗费的时间比较多,结果是,大部分的时间都浪费在了线程对象的创建和死亡上。
如果任务本身破坏力比较大,可能会把线程对象结束掉,就无法继续完成任务。
有线程池的状态:
在没有任务的时候,先把线程对象准备好,存储到一个容器中,一旦有任务来的时候,就不需要创建对象,而是直接将对象取出来执行任务。
如果任务破坏力较小,任务可以直接完成,这个线程对象不会进入死亡状态,而是被容器回收,继续活跃。
如果任务破坏力较大,任务会把线程搞死,线程池会继续提供下一个线程,继续完成这个任务。
与数据库连接池类似,线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象传给线程池,线程池就会启动一条线程来执行该对象的run方法。当run方法执行结束后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个Runnable对象的run方法。
除此之外,使用线程池可以有效地控制系统中并发线程的数量,当系统中包含大量并发线程时,会导致系统性能剧烈下降,甚至导致JVM崩溃,而线程池的最大线程数可以控制系统中并发线程数目不超过此数目。
JDK1.5以前,必须手动实现自己的线程池,从JDK1.5开始,Java内建支持线程池。
2.线程池创建方式
工具类:Executors:生成线程池的工具类,根据需求生成指定大小的线程池。
static ExecutorService | 创建一个根据需要创建新线程的线程池,但在可用时将重新使用以前构造的线程。 |
static ExecutorService | newFixedThreadPool(int nThreads) 创建一个线程池,该线程池重用固定数量的从共享无界队列中运行的线程。 |
static ExecutorService | 创建一个使用从无界队列运行的单个工作线程的执行程序。 |
static ScheduledExecutorService | newScheduledThreadPool(int corePoolSize) 创建一个线程池,可以调度命令在给定的延迟之后运行,或定期执行。 |
static ScheduledExecutorService | newSingleThreadScheduledExecutor() 创建一个单线程执行器,可以调度命令在给定的延迟之后运行,或定期执行。 |
以上五个方法的前三个方法返回一个ExecutorService对象,该对象代表一个线程池,它可以执行Runnable或Callable(后面介绍)对象所代表的线程。而后两个方法返回一个ScheduledExecutorService线程池,它是ExecutorService的子类,它可以指定延迟后执行线程任务。
3.ExecutorService
ExecutorService代表尽快执行线程的线程池(只要线程池中有空闲线程立即执行线程任务), 程序只要将一个Runnable对象或Callable对象提交给该线程池即可,该线程池就会尽快执行该任务。ExecutorService里提供了三个方法:
Future<?> | submit(Runnable task) 提交一个可运行的任务执行,并返回一个表示该任务的未来。 |
<T> Future<T> | submit(Runnable task, T result) 提交一个可运行的任务执行,并返回一个表示该任务的未来。 |
<T> Future<T> | submit(Callable<T> task) 提交值返回任务以执行,并返回代表任务待处理结果的Future。 |
当用完一个线程池后,应该调用关闭线程池的方法。
shutdown():调用这个方法后,线程池将不会再接受新任务,但原有的任务会继续执行,直到执行完成。
shutdownNow():调用这个方法关闭线程池,没有执行完成的任务会暂停。
代码示例
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test {
public static void main(String[] args) {
//获取一个线程池
ExecutorService es = Executors.newFixedThreadPool(2);
//准备一个任务类对象
Runnable r1 = new Runnable() {
@Override
public void run() {
for(int i=0;i<10;i++) {
System.out.println(Thread.currentThread().getName()+"...."+i);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
//准备一个任务类对象
Runnable r2 = new Runnable() {
@Override
public void run() {
for(int i=0;i<10;i++) {
System.out.println(Thread.currentThread().getName()+"...."+i);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
};
//准备一个任务类对象
Runnable r3 = new Runnable() {
@Override
public void run() {
for(int i=0;i<10;i++) {
System.out.println(Thread.currentThread().getName()+"...."+i);
try {
Thread.sleep(50);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
};
es.submit(r1);
es.submit(r2);
es.submit(r3);
//es.shutdown();
//哪个任务线程先执行完,就去执行任务3,如果没有shutdown,任务执行完,程序不会终止运行。
}
}
九、单例设计模式
在当前系统中,某个类型的对象,最多只能有一个,就需要使用单例设计模式。
单例模式的设计原则:
1、构造方法私有化
2、在类中创建好该类对象
3、在类中,给外界提供获取该对象的公有方式
1.饿汉式
在加载类的同时,就要初始化静态成员变量,所以就同时将该类对象创建出来了。
饿汉式:一有机会,马上就吃,不去等待。(一旦加载类型,马上创建对象)
代码示例
public class Test {
public static void main(String[] args) {
Single s = Single.getInstance();
Single s1 = Single.getInstance();
System.out.println(s==s1);//true
}
}
class Single{
private Single() {//私有构造方法
}
private static Single s = new Single();//直接创建对象
public static Single getInstance() {
return s;
}
}
2.懒汉式
在加载类的时候,不同时创建该类对象,等到需要获取这个对象时,才去创建这个对象
懒汉式:不着急、能不创建的时候,就不创建,能拖就拖。
注意事项:
1、只有在sl == null的时候,才会创建对象。
2、sl的判断和sl的赋值,不希望分离开,否则在多线程环境下,会出现多个对象的状态,所以sl的判断和sl的赋值,需要放到一个同步代码块中。
3、同步代码块的效率非常低,不是每次获取对象的时候,都需要判断锁对象,只有在sl为null的时候, 才应该判断锁对象,因此在外层需要嵌套一个if判断,判断sl是否为null。
单例模式-->懒汉式--->存在安全问题--->怎么解决--->加上同步代码--->效率如何保证--->在同步代码块外层判断是否为null.
代码示例
public class Test {
public static void main(String[] args) {
SingleDemo sd1 = SingleDemo.getInstance();
SingleDemo sd2 = SingleDemo.getInstance();
System.out.println(sd1==sd2);//true
}
}
class SingleDemo{
//构造方法私有化
private SingleDemo() {
}
private static SingleDemo sd;
public static SingleDemo getInstance() {
//t1 t2
if(sd==null) {
synchronized (SingleDemo.class) {
if(sd==null) {
sd = new SingleDemo();
}
}
}
return sd;
}
}
3.老汉式
也是随着类的加载,而创建对象(也算是饿汉式的一种)。
不再提供访问私有成员变量的方法。
指向对象的引用设置为公有的访问权限,但是这样可能会被外界修改,因此在引用前面加上final,外界只能访问该变量,不能修改该变量的值。
代码示例
public class Test {
public static void main(String[] args) {
Single1 s1 = Single1.s1;
Single1 s2 = Single1.s1;
System.out.println(s1==s2);//true
}
}
class Single1{
private Single1() {}
public static final Single1 s1 = new Single1();
}
十、垃圾回收
1.概念
当对象被创建时,就会在Java虚拟机的堆区中拥有一块内存,在Java虚拟机的生命周期中,Java程序会陆续地创建无数个对象,假如所有的对象都永久占有内存,那么内存有可能很快被消耗光,最后引发内存空间不足的错误。因此必须采取一种措施来及时回收那些无用对象的内存,以保证内存可以被重复利用。
在一些传统的编程语言(如C语言)中,回收内存的任务是由程序本身负责的。程序可以显式地为自己的变量分配一块内存空间,当这些变量不再有用时,程序必须显式地释放变量所占用的内存。把直接操纵内存的权利赋给程序,尽管给程序带来了很多灵活性,但是也会导致以下弊端:
程序员有可能因为粗心大意,忘记及时释放无用变量的内存,从而影响程序的健壮性。也有可能错误地释放核心类库所占用的内存,导致系统崩溃。
Java语言中,内存回收的任务由Java虚拟机来担当,而不是由Java程序来负责。在程序的运行时环境中,Java虚拟机提供了一个系统级的垃圾回收器线程,它负责自动回收那些无用对象所占用的内存,这种内存回收的过程被称为垃圾回收(Garbage Collection)。
垃圾回收具有以下优点:
把程序员从复杂的内存追踪、监测和释放等工作中解放出来,减轻程序员进行内存管理的负担。
防止系统内存被非法释放,从而使系统更加健壮和稳定。
垃圾回收特点:
只有当对象不再被程序中的任何引用变量引用时,它的内存才可能被回收。
程序无法迫使垃圾回收器立即执行垃圾回收操作。
当垃圾回收器将要回收无用对象的内存时,先调用该对象的finalize()方法,该方法有可能使对象复活,导致垃圾回收器取消回收该对象的内存。
2.常见方法
System.gc()方法:告诉JVM垃圾回收,但是至于JVM是否会回收是没办法控制的。
finalize()方法:类中定义finalize()方法的含义是,当这个类的对象被垃圾回收时,会执行finalize()方法。
代码示例
public class Test {
public static void main(String[] args){
User u1 = new User(); // 栈区的u1 指向堆区的对象
u1 = new User(); // u1重写指向了堆区中的另外一个对象,原来指向的对象就没有引用指向它了,就变成了垃圾
System.gc(); // 这个方法的含义是督促JVM进行垃圾回收工作,但不是强制。
}
}
class User{
private int id;
private String name;
private int age;
public User() {
super();
}
public User(int id, String name, int age) {
super();
this.id = id;
this.name = name;
this.age = age;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
protected void finalize() throws Throwable {
System.out.println("这个方法会在垃圾回收时被调用——回收前");
}
}
十一、枚举类型
1.概述
枚举其实就是一种特殊的类,一般用于表示多种固定的状态。单例设计模式,设计出来的是某个类型的对象只有一个;枚举类型就是有多个对象的单例设计模式,也可以称为多例模式。
声明一个枚举类型,使用的关键字是enum,声明出来的也是一个类,编译出来也是一个.class的文件,只不过是补充了一些内容。
格式:
public enum 名字{
枚举项1,枚举项2;
}
2.枚举实现
需求:定义一个WeekDay类,使用成员变量来表示星期几(周一到周日)。
1.自己实现需求的第一种方式
(1)使用定义常量的方式。
public class Test {
public static void main(String[] args) {
WeekDay wd = WeekDay.TUE;
System.out.println(wd);
}
}
class WeekDay{
private WeekDay(){ // 私有构造方法,保证在外部不能创建对象
}
//提供了7个固定的对象 ------ 多例模式
public static final WeekDay MON = new WeekDay();
public static final WeekDay TUE = new WeekDay();
public static final WeekDay WEN = new WeekDay();
public static final WeekDay THR = new WeekDay();
public static final WeekDay FRI = new WeekDay();
public static final WeekDay SAT = new WeekDay();
public static final WeekDay SUN = new WeekDay();
}
(2)使用枚举方式实现
public class Test {
public static void main(String[] args) {
WeekDay12 tue = WeekDay12.TUE;
System.out.println(tue);//TUE
}
}
enum WeekDay12{
MON,TUE,WEN,THU,FRI,SAT,SUN;
}
2.自己实现的第一种方式改进
在上面的第一种实现方式中,对比自己实现的方式和使用枚举的实现方式。
自己实现方式,每一项对应一个数字。例如:MON 对应数字 1。
枚举方式实现,每一个枚举项对应一个结果。例如:MON 对应MON。
修改上面的需求: 无论是自己实现的方式,还是枚举实现的方式都对应汉字、
例如: MON对应星期一,TUE对应星期二 ......
(1)在WeekDay中添加成员变量解决以上问题。
public class Test{
public static void main(String[] args) {
System.out.println(WeekDay.MON.getName());//星期一
}
}
class WeekDay {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
private WeekDay(String name){
this.name = name;
}
public static final WeekDay MON = new WeekDay("星期一");
public static final WeekDay TUE = new WeekDay("星期二");
public static final WeekDay WEN = new WeekDay("星期三");
public static final WeekDay THU = new WeekDay("星期四");
public static final WeekDay FRI = new WeekDay("星期五");
public static final WeekDay SAT = new WeekDay("星期六");
public static final WeekDay SUN = new WeekDay("星期日");
}
(2) 使用枚举方式进行改进
public class Test{
public static void main(String[] args) {
System.out.println(WeekDay.MON.getName());//星期一
}
}
enum WeekDay {
MON("星期一"),TUE("星期二"),WEN("星期三"),THU("星期四"),FRI("星期五"),SAT("星期六"),SUN("星期日");
private WeekDay(String name){ //注意,构造方式必须是私有的
this.name = name;
}
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
3.添加抽象方法
(1)在自己实现的类中添加抽象方法,类也要被声明为抽象类。
public class Test{
public static void main(String[] args) {
System.out.println(WeekDay.MON.getName());//星期一
WeekDay.TUE.sayHello();//你好,我是:星期二
}
}
abstract class WeekDay {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public WeekDay(String name){
this.name = name;
}
public static final WeekDay MON = new WeekDay("星期一"){
public void sayHello() {
System.out.println("你好,我是:"+getName());
}
};
public static final WeekDay TUE = new WeekDay("星期二"){
public void sayHello() {
System.out.println("你好,我是:"+getName());
}
};
public static final WeekDay WEN = new WeekDay("星期三"){
public void sayHello() {
System.out.println("你好,我是:"+getName());
}
};
public static final WeekDay THU = new WeekDay("星期四"){
public void sayHello() {
System.out.println("你好,我是:"+getName());
}
};
public static final WeekDay FRI = new WeekDay("星期五"){
public void sayHello() {
System.out.println("你好,我是:"+getName());
}
};
public static final WeekDay SAT = new WeekDay("星期六"){
public void sayHello() {
System.out.println("你好,我是:"+getName());
}
};
public static final WeekDay SUN = new WeekDay("星期日"){
public void sayHello() {
System.out.println("你好,我是:"+getName());
}
};
public abstract void sayHello(); // 添加抽象方法
}
(2)枚举类中添加抽象方法
public class Test{
public static void main(String[] args) {
System.out.println(WeekDay.MON.getName());//星期一
WeekDay.TUE.sayHello();//大家好,我是:星期二
}
}
enum WeekDay {
MON("星期一"){
public void sayHello(){
System.out.println("大家好,我是:"+getName());
}
}
,TUE("星期二"){
public void sayHello(){
System.out.println("大家好,我是:"+getName());
}
}
,WEN("星期三"){
public void sayHello(){
System.out.println("大家好,我是:"+getName());
}
}
,THU("星期四"){
public void sayHello(){
System.out.println("大家好,我是:"+getName());
}
}
,FRI("星期五"){
public void sayHello(){
System.out.println("大家好,我是:"+getName());
}
}
,SAT("星期六"){
public void sayHello(){
System.out.println("大家好,我是:"+getName());
}
}
,SUN("星期日"){
public void sayHello(){
System.out.println("大家好,我是:"+getName());
}
};
private WeekDay(String name){ //注意,构造方式必须是私有的
this.name = name;
}
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public abstract void sayHello();
}
3.枚举类型的注意事项
定义枚举类型必须使用enum关键字,创建的其实也是一个普通的类型。
所有的枚举项,必须定义在枚举类型的第一行(第一个分号之前),枚举项之间使用逗号分隔,最后一个枚举项之后需要使用分号结尾。
枚举类型也有构造方法,只能默认提供空参构造,需要我们手动定义有参构造。在枚举类型中,所有的构造方法,都必须是私有化的。
枚举类型也可以有抽象方法,但是必须在枚举项中,将该方法实现。
4.枚举类型的常用方法
int | compareTo(E o) 将此枚举与指定的对象进行比较以进行订购。 | ||
int | ordinal() 返回此枚举常数的序数(其枚举声明中的位置,其中初始常数的序数为零)。 | ||
String | name() 返回此枚举常量的名称,与其枚举声明中声明的完全相同。 | ||
String | toString() 返回声明中包含的此枚举常量的名称。 |
values():返回该枚举类型的所有枚举项(API中没有,但是可以调用)
代码示例
public class Test {
public static void main(String[] args) {
System.out.println(Demo.D1.ordinal());//0
System.out.println(Demo.D2.ordinal());//1
System.out.println(Demo.D3.ordinal());//2
System.out.println(Demo.D2.compareTo(Demo.D1));//1
System.out.println(Demo.D1.name());//D1
System.out.println(Demo.D1);//d1
Demo[] values = Demo.values();
for(Demo d:values) {
System.out.println(d);//d1 d2 d3
}
}
}
enum Demo{
D1,D2,D3;
public String toString() {
return this.name().toLowerCase();
}
}