Java入门学习笔记——郝斌(三)线程

1、 toString方法

  • 所有的类都默认自动继承了Object
  • Object类中的toString方法返回的是类的名字和该对象哈希码组成的字符串
  • System.out.println(类对象名);
    实际输出的是该对象的toString()方法所返回的字符串
  • 为了实际需要,建议子类重写从父类Objec继承的toString方法

System.out.println(类对象名);示例1

class A
{
}
public class TestToString
{
	public static void main(String[] args)
	{	
		A aa = new A();
		System.out.println(aa);
	}
}

输出结果是:
输出结果

class A
{
	public String toString() // 重写了父类Object中的toString方法
	{
		return "haha";
	}
}
public class TestToString_1
{
	public static void main(String[] args)
	{
		A aa = new A();
		System.out.println(aa);
	}
}

输出结果:
输出结果

System.out.println(类对象名);示例2

class Point
{
	private int i, j;
	public Point(int i, int j)
	{
		this.i = i;
		this.j = j;
	}
	public String toString(){}
	{
		return "[" + i + ", " + j+ "]";  //输出[1, 2]
	}
}
public class TestToString_2
{
	public static void main(String[] args)
	{
		Point pp1 = new Point(1, 2);
		System.out.println(pp1);
	}
}

String与StringBuffer

Object类的equals方法

  • 所有类都从Object类中继承了equals方法
  • Object类中equals方法源代码如下:
    public boolean equals(Object obj)
    {
    	return this == obj;
    }
    
  • Object中的equals方法是直接判断thisobj本身的值是否相等,即用来判断调用equals的对象和形参obj所引用的对象是否是同一对象,所谓同一对象就是指内存中同一块存储单元,如果thisobj指向的是同一块内存对象,则返回true,如果thisobj指向的不是同一块内存,则返回false,注意:即便是内容完全相等的两块不同的内存对象,也回返回false
  • 如果是同一块内存,则Object中的equals方法返回true,如果是不同的内存,则返回false

何时需要重写equals方法

  • 用一个类构造出来的不同内存的两个对象,如果内存中的值相等,我们一般情况下也应该认为这两个对象相等,很明显Object中的equals()无法完成这样的重任,Object中的equals()方法只有在两个对象是同一块内存时,才返回true,这时候我们就有必要重写父类Object中的equals方法
  • 如果希望不同内存但相同内容的两个对象equals时返回true,则我们需要重写父类的equals方法
class A
{
	public int i = 10;
}
public class TestObjectEquals
{
	public static void main(String[] args)
	{
		A aa1 = new A();
		A aa2 = new A();
		if (aa1.equals(aa2))
		{
			System.out.println("aa1 == aa2");
		}
		else
		{
			System.out.println("aa1 != aa2"); // aa1和aa2分配的是两块不同的内存,但这两块内存的值都是一样的,尽管如此,还是返回false,这从本语句会输出
		}
		aa1 = aa2;
		if (aa1.equals(aa2))
		{
			System.out.println("aa1 == aa2"); //输出aa1 == aa2
		}
		else
		{
			System.out.println("aa1 != aa2");
		}
	}
}
//运行结果:
//aa1 != aa2
//aa1 == aa2
class A
{
	public int i;
	public A(int i)
	{
		this.i = i;
	}
}
public class TestEquals
{
	public static void main(String[] main)
	{
		A aa1 = new A(1);
		A aa2 = new A(1);
		System.out.println(aa1 == aa2);
		System.out.println(aa1.equals(aa2));
	}
}
//输出结果:
//false
//false

String类

  • java.lang.String类对象表示不可修改的Unicode编码字符串。
  • Java中双引号括起来的字符串也被当做String对象
    • System.out.println("abc".length()); //输出3
    • System.out.println("abc".equals("abc")); //输出true

String类的equals方法

  • String类已经重写了Objectequals方法
  • 例子:
    • 假设str1str2都是String对象
    • str1.equals(str2):是用来比较str1变量本身所占内存的值所指向的对象和str2变量本身所占内存的值所指向的对象的内容是否相等,如果相等则返回true,否则返回true
  • String类的equals方法是用来判断两个对象的内容是否相等,Object类的equals方法是用判断两个对象是否是同一个对象
  • 一定要注意 ==equals的区别
public class TestString_1
{
	public static void main(String[] args)
	{
		String str1 = new String("china"); //str1 和 str2 很明显指向的是不同的对象
		String str2 = new String("china");
		System.out.println(str1.equals(str2)); //true  判断str1 与 str2所引用的对象是否相等  很明显是true
		System.out.println(str1 == str2); //false
		
		String str3 = "china"; //str3 和 str4 都指向了匿名对象"china"
		String str4 = "china";
		System.out.println(str3.equals(str4)); //true
		System.out.println(str3 == str4); //true 判断str3 和 str4本身的内容是否相等
	}
}

总结:
str1 == str2是用来比较str1变量本身所占内存的值和str2变量本身所占内存的值是否相等

str1.equals(str2) 是用来比较str1变量本身所占内存的值所指向的对象和str2变量本身所占内存的值所指向的对象的内容是否相等

也就是说如果str1str2占用不同的内存,但是这两个不同内存的值是一样的,String.equals()方法也会返回true。但是Object中的equals()方法却是返回false

Object中就含有equals()方法,不过String类重写了Object中的equals()方法
String类示例内存示意图

String类常用方法

public char charAt(int index) 返回字符串中第index个字符
public int length() 返回字符串的长度
public int indexOf(String str) 返回字符串中出现str的第一个位置
public int indexOf(String str, int fromIndex) 返回字符串中从fromIndex开始出现str的第一个位置
public boolean equalsIgnoreCase(String another) 比较字符串与another是否一样(忽略大小写)
public String replace(char oldChar, char newChar) 在字符串中用newChar字符替换oldChar字符

public boolean startsWith(String prefix) 判断字符串是否以prefix字符串开头
public boolean endsWith(String suffix) 判断字符串是否以suffix字符串结尾
public String toUpperCase() 返回一个字符串为该字符串的大写形式
public String toLowerCase() 返回一个字符串为该字符串的小写形式
public String substring(int beginIndex) 返回该字符串从beginIndex开始到结尾的子字符串
public String substring(int beginIndex, int endIndex) 返回该字符串从beginIndex开始到endIndex结尾的子字符串

  • 静态重载方法
    public static String valueOf(...)可以将基本类型数据转换为字符串;
    例如:

    • public static String valueOf(double d)
    • public static String valueOf(int i)

    方法public String[] split(String regex)可以将一个字符串按照指定的分隔符分隔,返回分隔后的字符串数组。

//String举例
public class Test
{
	public static void main(String[] args)
	{
		String s1 = "sun java", s2 = "Sun Java";
		System.out.println(s1.charAt(1)); //u
		System.out.println(s2.length()); //8
		System.out.println(s1.indexOf("java")); //4
		System.out.println(s1.indexOf("Java")); //-1
		System.out.println(s1.equals(s2)); //false
		System.out.println(s1.equalsIgnoreCase(s2)); //true

		String s = "我是程序员,我在学java";
		String sr = s.replace('我', '你');
		System.out.println(sr); //你是程序员,你在学java
	}
}
public class Test02
{
	public static void main(String[] args)
	{
		String s = "Welcome to Java World!";
		String s1 = " sun java ";
		System.out.println(s.startsWith("Welcome")); //true
		System.out.println(s.endsWith("World")); //false
		String sL = s.toLowerCase();
		String sU = s.toUpperCase();
		System.out.println(sL); //welcome to java world!
		System.out.println(sU); //WELCOME TO JAVA WORLD!
		String subs = s.substring(11); 
		System.out.println(subs); //Java World!
		String sp = s1.trim(); 
		System.out.println(sp); //sun java
	}
}
public class Test03
{
	public static void main(String[] args)
	{
		int i = 1234567;
		String sNumber = String.valueOf(i);
		System.out.println("j 是 "+sNumber.length() + "位数。");
		String s = "Mary,F,1976";
		String[] sPlit = s.split(",");
		for (i = 0; i<sPlit.length; i++)
		{
			System.out.println(sPlit[i]);
		}
	}
}
//输出结果
//j是 7位数
//Mary
//F
//1976

StringBuffer类

StringBuffer类由来

  • String类对象一旦创建就不可更改
  • 如果经常对字符串内容进行修改,则使用StringBuffer
  • 如果经常对字符串内容进行修改而使用String的话,就会导致既耗空间又耗时间
  • 例子
    • String s1 = "abasddffnjh"; String s2 = "123";
    • String s1 = s1 + s2;
    • 删除s1中的字母d
  • StringBuffer对象的内容是可以改变的
  • 因此String类中没有修改字符串的方法,但是StringBuffer类中却有大量修改字符串的方法
//只使用String类来删除字符串对象中的第一个n这个字母的方法
public class Test
{
	public static void main(String[] args)
	{
		String s1 = "zhangsan";
		String s2 = s1.substring(0, 3);
		String s3 = s1.substring(4, s1.length());
		s1 = s2 + s3;
		System.out.printf("s1: %s", s1);
	}
}

StringBuffer类的构造函数

  • public StringBuffer() 创建一个空的没有任何字符的StringBuffer对象
  • public StringBuffer(int capacity) 创建一个不带字符,但具有指定初始容量的字符串缓冲区
  • public Stringbuffer(String str) 创建一个StringBuffer对象,包含于str对象相同的字符序列
StringBuffer常用方法
  • 重载方法public StringBuffer append(...) 可以为该StringBuffer对象添加字符序列,返回添加后的该StringBuffer对象引用,例如:
    • public StringBuffer append(String str)
    • public StringBuffer append(StringBuffer str)
    • public StringBuffer append(char[] str)
    • public StringBuffer append(char[] str, int offset, int len)
    • public StringBuffer append(double d)
    • public StringBuffer append(Object obj)
  • 重载方法public StringBuffer insert(...) 可以为该StringBuffer 对象在指定位置插入字符序列,返回修改后的该StringBuffer对象引用,例如:
    • public StringBuffer insert(int offset, String str)
    • public StringBuffer insert(int offset, double d)
  • 方法public StringBuffer delete(int start, int end) 可以删除从start开始到end-1为止的一段字符序列,返回修改后的该StringBuffer 对象引用。
  • String 类含义类似的方法:
    • public int indexOf(String str)
    • public int indexOf(String str, int fromIndex)
    • public String substring(int start)
    • public String substring(int start, int end)
    • public int length()
  • 方法public StringBuffer reverse()用于将字符序列逆序,返回修改后的该StringBuffer对象引用
//StringBuffer举例
public class TestStringBuffer
{
	public static void main(String[] args)
	{
		StringBuffer sb = new StringBuffer();
		sb.append("abc");
		sb.append("123");
		System.out.println("sb = " + sb); //sb = abc123
		sb.insert(3, "--");
		System.out.println("sb = " + sb); //sb = abc--123
		sb.delete(2, 6); //把下标从2开始到6-1结束的字符删掉
		System.out.println("sb = " + sb); //sb = ab23
		sb.reverse();
		System.out.println("sb = " + sb); //sb = 32ba
		String str = sb.toString();
		System.out.println("str = " + str); //str = 32ba
	}
}

2、数组

一维数组的使用

public class TestArray_1
{
	public static void main(String[] args)
	{
		//方式一
		int[] arr1; //等同于int arr1[];
		arr1 = new int[3];
		arr1[0] = 0;
		arr1[1] = 1;
		arr1[2] = 2;
		showArr(arr1);
		System.out.println("***************");
		
		//方式二
		int[] arr2 = new int[]{0, 1, 2};
		showArr(arr2);
		System.out.println("***************");
		//System.out.println(arr2); //error 一维数组的内容是不能通过System.out.println()直接输出的,即便该数组的内容是引用且已经重写了toString方法也不行

		//int[3] arr3 = new int[]{0, 1, 2};  //error
		//int[] arr4 = new int[3]{0,1,2};  //error
		//int[3] arr5 = new int[3]{0,1,2};  //error

		//方法三
		int[] arr6 = {0, 1, 2};
		showArr(arr6);
		System.out.println("***********");
		arr6 = new int[]{5, 4, 3, 2, 1};
		showArr(arr6);
	}
	public static void showArr(int[] arr)
	{
		for (int i = 0; i<arr.length;++i)
			System.out.println(arr[i]);
	}
}

public static void arraycopy(Object arr1, int pos1, Object arr2, int pos2, int length);
arr1所指向的数组中下标从pos1开始的总共length个元素覆盖掉arr2所指向的数组中从pos2开始的length个元素

  • arr1是源数组,arr2是目的数组
  • arraycopy() 全是小写,不能是大写
//System.arraycopy();的用法
class TestArrayCopy
{
	public static void main(String[] args)
	{
		int[] a = {1, 2, 3, 4, 5};
		int[] b = {-1, -2, -3, -4, -5};
		System.arraycopy(a, 0, b, 1, 2); //-1 1 2 -4 -5
		
		System.out.println("a = ");
		for (int i=0; i<a.length; ++i)
		{
			System.out.println(a[i]);
		}

		System.out.println("b = ");
		for (int i=0; i<b.length; ++i)
		{
			System.out.println(b[i]);
		}
		System.out.println("Hello World!");
	}
}
/*运行结果
a = 
1
2
3
4
5
b = 
-1
1
2
-4
-5
Hello World!
*/
//数组的排序
import java.util.*;
public class TestArraySort_1
{
	public static void main(String[] args)
	{
		int[] data = {1, 3, 5, 7, 2, 4, 6, 8, 10, 9};
		System.out.println("排序前数组data中的内容是:");
		showArray(data);

		Arrays.sort(data);

		System.out.println("排序后数组data中的内容是:");
		showArray(data);
	}
	public static void showArray(int[] data)
	{
		for (int e : data)
			System.out.printf("%d\t", e);
		System.out.println("");
	}
}

3、线程

初学者要注意四个问题

  • 什么是进程(尽量理解)
  • 为什么需要进程(理解)
  • 什么是线程(必须掌握)
  • 为什么需要线程(不知道也没关系)

程序的定义

所谓“程序”,是一个严格有序的指令集合。程序规定了完成某一任务时,计算机所需做的各种操作,以及这些操作的执行顺序。

单道程序设计环境中程序特点

  • 单道程序设计环境是指:计算机中除了操作系统之外,只存在一个用户程序,即用户程序独享整个计算机资源
  • 单道程序有如下特点:
    • 资源的独占性:任何时候,位于内存中的程序可以使用系统中的一切资源,不可能有其他程序与之竞争
    • 执行的顺序性:内存中每次只有一个程序,各个程序是按序执行的,即做完一个,再做下一个
      绝对不可能出现在一个程序运行过程中,又夹杂进另一个程序执行的现象存在
    • 结果的再现性:只要执行环境和初始条件相同,重复执行一个程序,获得的结果总是一样的。

多道程序设计环境中程序特点

  • 多道程序设计是指:计算机中除了操作系统之外,存在多个用户程序,这些程序同时运行
  • 多道程序设计有如下特点:
    • 间断性:由于资源共享和合作,并发程序间相互制约,造成合作执行间断
    • 失去封闭性:程序执行受外界影响
    • 不可再现性:重复执行时,可能得到不同结果

进程的由来

  • 一方面为了保持程序“是一个在时间上严格有序的指令集合,是静态的保存在存储介质上”这个概念的原有含义,另一方面为了刻画多个程序共同运行时呈现出的这些特征,在操作系统中,以“程序”为基础,又引入了“进程”这一新的概念
  • 通俗点说,为了不破坏“程序这个词原有的含义,而又能刻画多个程序共同运行时呈现出的新特征,所以引入了进程这一概念”,按照大多数教材的说法:“为了使程序能并发执行,且为了对并发执行的程序加以描述,所以人们引入了进程”

进程的定义

至今都没有统一的说法

  • 程序只是一组指令的有序集合,它本身没有任何运行的含义,他只是一个静态的实体
  • 进程是程序在某个数据集上的执行。进程是一个动态的实体, 因为他有自己的生命周期。它因创建而产生,因调度而运行,因等待资源或事件而被处于等待状态,因完成任务而被撤销。

线程的定义

  • 线程是一个程序里的不同执行路径
  • 以前所编写的程序,每个程序都有一个入口、一个出口以及一个顺序执行的序列,在程序执行过程中的任何指定时刻,都只有一个单独的执行点。
  • 事实上,在单个程序内部是可以在同一时刻进行多种运算的,这就是所谓的多线程
  • 程序、进程、线程的异同参见操作系统,这些并不影响我们对Java的学习
public class ThreadDef1
{
	public static void main(String[] args)
	{
		A aa = new A();
		aa.run();
		while(true){
			System.out.println("BBBB"); //得不到输出
		}
	}
}
class A extends Thread
{
	public void run()
	{
		while (true){
			System.out.println("AAAA"); //一直输出AAAA,
		}
	}
}

多线程的优势:

  • 多线程编程简单,效率高(能直接共享数据的资源,多进程不能)
  • 适合于开发服务程序(如Web服务,聊天服务等)

如何创建一个线程

创建一个线程的第一种方法

  1. 创建一个继承Thread的类(假定类名为A),并重写Thread中的run方法
  2. 构造一个A类对象,假定对象名为aa
  3. 调用aastart方法(start方法是从Thread继承过来的)
//创建一个线程的第一种方法
public class ThreadDef1
{
	public static void main(String[] args)
	{
		A aa = new A(); //构造一个A类对象
		aa.run(); //调用aa的start方法,start方法会创建一个新的线程,并自动调用aa对象的run()方法
		while(true){
			System.out.println("BBBB"); //得不到输出
		}
	}
}
class A extends Thread //创建一个继承Thread的类
{
	public void run() //重写Thread中的run方法
	{
		while (true){
			System.out.println("AAAA"); //一直输出AAAA,
		}
	}
}
注意问题:
  • Threadstart()方法的功能就是创建一个新的线程,并自动调用该线程的run()方法,直接调用run()方法是不会创建一个新的线程的
  • 执行一个线程实际就是执行该线程run方法中的代码
  • 执行完aa.start();后并不表示aa所对应的线程就一定会立即得到了执行,aa.start();执行完后只是表示aa线程具有了可以立即被CPU执行的资格,但由于想抢占CPU执行的线程很多,CPU并不一定会立即去执行aa所对应的线程
  • 一个Thread对象能且只能代表一个线程,一个Thread对象不能调用两次start()方法,否则会抛出java.lang.IllegalThreadStateException异常
/*	执行完aa.start();后并不表示aa所对象的线程就一定会立即得到了执行,
	aa.start();执行完后只是表示t线程具有了可以立即被CPU执行的资格,
	但由于想抢占CPU执行的线程很多,CPU并不一定会立即去执行t所对应的线程
*/
public class TestThreadStart
{
	public static void main(String[] args)
	{
		A aa = new A();
		aa.start();  //执行完本语句后并不表示说CPU一定会立即开启一个线程,转去执行这个线程的代码
		System.out.println("哈哈");
		for (int i=0; i<10; ++i)
		{
			System.out.println(i + "11");
		}
	}
}
class A extends Thread  //创建一个继承Thread的类
{
	public void run()  //重写Thread中的run方法
	{
		for (int i=0; i<10; ++i)
		{
			System.out.println(i + "22");
		}
	}
}
/*  一个Thread对象只能代表一个线程并且只能代表一个线程,
	一个Thread对象不能调用两次start()方法,
	否则会抛出java.lang.IllegalThreadStateException异常
*/
public class TestThreadStart2
{
	public static void main(String[] agrs)
	{
		A aa = new A();
		aa.start();
		try
		{
			aa.start(); //一定要明白这是主线程中的语句,这是主线程中可能会抛出的异常,此时很可能aa所对应的线程已经在执行
		}
		catch (java.lang.IllegalThreadStateException e)
		{
			System.out.println("一个线程不能启动多次");
			System.out.println("一个线程不能启动多次");
			System.out.println("一个线程不能启动多次");
			System.exit(-1);
		}
		while(true)
		{
			System.out.println("BBBB");
		}
	}
}
class A extends Thread //创建一个继承Thread的类
{
	public void run() //重写Thread中的run方法
	{
		while(true)
		{
			System.out.println("AAAA");
		}
	}
}
/*
	在JDK中的某一次运行的具体结果是:
----------------------------------
一个线程不能启动多次
一个线程不能启动多次
AAAA
AAAA
AAAA
AAAA
AAAA
AAAA
AAAA
AAAA
AAAA
AAAA
一个线程不能启动多次
AAAA
AAAA
----------------------------------
*/

线程状态的切换

线程状态的切换
执行完aa.start();后并不表示aa所对应的线程就一定会立即得到了执行,aa.start();执行完后只是表示aa线程具有了可以立即被CPU执行的资格,但由于想抢占CPU执行的线程很多,CPU并不一定会立即去执行aa所对应的线程

创建一个新线程的第二种方法

  1. 定义一个实现了Runnable接口的类,假定为A
  2. 创建A类对象aa,代码如下
    A aa = new A();
  3. 利用aa构造一个Thread对象tt
    Thread tt = new Thread(aa);
  4. 调用tt中的start方法
    tt.start();
public class ThreadTest
{
	public static void main(String[] args)
	{
		A aa = new A(); //创建A类对象aa
		Thread tt = new Thread(aa); //利用aa构造一个Thread对象tt
		tt.start(); //调用tt中的start()方法
		while(true)
		{
			System.out.println("BBBB");
		}
	}
}
class A implements Runnable //定义了一个实现了Runnable接口的类A
{
	public void run()
	{
		while(true)
		{
			System.out.println("AAAA");
		}
	}
}

Thread的常用方法

  • public final void setName(String name) 设置当前线程的名字
  • public static Thread currentThread() 返回对当前正在执行的线程对象的引用
  • public final String getName() 返回当前线程的名字
public class TestThreadAPI
{
	public static void main(String[] args)
	{
		T t = new T();
		t.setName("java");
		t.start();
		for (int i=0; i<30; ++i)
		{
			System.out.printf("嘿嘿\n");
			System.out.printf("%s线程被调用了\n", Thread.currentThread().getName());
		}
	}
}
class T extends Thread
{
	public void run()
	{
		for (int i=0; i<30; ++i)
		{
			System.out.printf("哈哈\n");
			System.out.printf("%s线程被调用了\n", Thread.currentThread().getName());
		}
	}
}

线程的控制

线程状态的切换

线程状态的切换
执行完aa.start();后并不表示aa所对应的线程就一定会立即得到了执行,aa.start();执行完后只是表示aa线程具有了可以立即被CPU执行的资格,但由于想抢占CPU执行的线程很多,CPU并不一定会立即去执行aa所对应的线程

线程控制的基本方法

方法功能
isAlive()判断线程是否还“活”着,即线程是否还未终止
getPriority()获得线程的优先级数值
setPriority()设置线程的优先级数值
Thread.sleep()将当前线程睡眠指定毫秒数
join()调用某线程的该方法,将当前线程与该线程“合并”,即等待该线程结束,再恢复当前线程的运行
yield()让出CPU,当前线程进入就绪队列等候调度
wait()当前线程进入对象的wait pool
notify()/notifyAll()唤醒对象的wait pool 中的一个/所有等待线程

优先级控制

线程优先级:

  • 线程的优先级用数字来表示,范围从1到10
  • 主线程的缺省优先级是5,子线程的优先级默认与其父线程相交
  1. Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程。线程调度器按照线程的优先级决定应调度哪个线程来执行
  2. 线程的优先级用数字表示,范围从1到10,一个线程的缺省优先级是5.
    • Thread.MIN_PRIORITY = 1
    • Thread.MAX_PRIORITY = 10
    • Thread.NORM_PRIORITY = 5
  3. 使用下述线方法获得或设置线程对象的优先级
    • int getPriority();
    • void setPriority(int newPriority);

通常高优先级的线程将先与优先级低的线程执行,但并不总是这样,因此实际开发中并不单纯依赖优先级来决定线程运行次序

public class TestPriority
{
	public static void main(String[] args)
	{
		Thread t1 = new Thread(new T1());
		Thread t2 = new Thread(new T2());
		//t1.setPriority(Thread.NORM_PRIORITY + 3);  //考虑把本语句注释掉后会怎样
		t1.start();
		t2.start();
	}
}
class T1 implements Runnable
{
	public void run()
	{
		for (int i=0; i<100; ++i)
		{
			System.out.println("T1: " + i);
		}
	}
}
class T2 implements Runnable
{
	public void run()
	{
		for (int i=0; i<100; ++i)
		{
			System.out.println("------T2: " + i);
		}
	}
}

线程的休眠和让步

线程的休眠
  • 线程休眠——暂停执行当前运行中的线程,使之进入阻塞状态,待经过指定的“延迟时间”后再醒来并转入到就绪状态
  • Thread类提供的相关方法:
    • public static void sleep(long millis)
    • public static void sleep(long milles, int nanos)
  • 由于是静态方法,可以由Thread直接调用
  • sleep()方法会抛出InterruptedException异常,我们必须得对其进行捕捉
public class TestSleep
{
	public static void main(String[] args)
	{
		T tt = new T();
		Thread aa = new Thread(tt);
		aa.start();
	}
}
class T implements Runnable
{
	public void run() //方法体内用了Thread.sleep(),不能再这throws Exception,因此被重写的run方法内没有throws Exception
	{
		for (int i=0; i<10; i++)
		{
			System.out.println(Thread.currentThread().getName() + " " + i);
			try{
				Thread.sleep(1000); //这里的Thread.sleep(1000)会抛出异常,必须对其进行捕捉
			}
			catch (Exception e){
				System.out.println(e);
			}
		}
	}
}

无论是继承Thread类的run方法还是实现Runnable接口的run方法,都不能抛出任何异常

class A extends Thread
{
	public void run() //throws Exception //注释符不能去掉,否则编译会报错
	{
	}
}
class B implements Runnable
{
	public void run() //throws Exception //注释符不能去掉,否则编译会报错
	{
	}
}

原因:重写方法抛出异常的范围不能大于被重写方法排除的异常范围

线程的让步
  • 让出CPU,给其他线程执行的机会
  • 让运行中的线程主动放弃当前获得的CPU处理机会,但不是使该线程阻塞,而是使之转入就绪状态。
    • public static void yield()
public class TestYield 
{
	public static void main(String[] args) {
	   MyThread mt = new MyThread();
	   Thread t1 = new Thread(mt);
	   Thread t2 = new Thread(mt);
	   t1.setName("线程A");
	   t2.setName("线程B");
	   t1.start();
	   t2.start();
	}
}
class MyThread implements Runnable 
{
	public void run(){
	    for(int i=1;i<=100;i++){
			System.out.println(Thread.currentThread().getName()+": "+i);
	      	if(0 == i%10){
	        	Thread.yield();
	      	}
	    }
  	}
}

线程的串行化

  • 在多线程程序中,如果在一个线程运行的过程中要用到另一个线程的运行结果,则可以进行线程的串型化处理
  • public final void join() throws InterruptedException
public class TestJoin
{
	public static void main(String[] args)
	{
		MyRun a = new MyRun();
		Thread t = new Thread(a);
		t.start();
		try
		{
			t.join(); //暂停当前正在执行t.join();的线程,直到t所对应的线程运行终止之后,当前线程才会获得继续执行的机会
				//注意:t.join() 不是暂停t对象所对应的线程
		}
		catch (Exception e){
			e.printStackTrace();
		}
		for (int i=0; i<40; i++)
		{
			System.out.println("主线程 :" + i);
		}
	}
}
class MyRun implements Runnable
{
	public void run(){
		for (int i=0; i<50; i++){
			System.out.println("子线程 :" + i);
		}
	}
}

挂起和恢复(已不提倡使用)

  • 线程挂起——暂时停止当前运行中的线程,使之转入阻塞状态,并且不会自动恢复运行
  • 线程恢复——使得一个已挂起的线程恢复运行

线程生命周期控制

如何结束一个线程:

public class TestShutThread
{
	public static void main(String[] args)
	{
		Runner r = new Runner();
		Thread t = new Thread(r);
		t.start();
		try{
			t.sleep(5000);
		}catch(Exception e){
			e.printStackTrace();
		}
		r.shutDown();
	}
}

class Runner implements Runnable
{
	private boolean flag = true;
	public void run()
	{
		while(flag)
		{
			System.out.println("AAAA");
		}
	}
	public void shutDown(){
		this.flag = false;
	}
}

线程同步

通过两个例子来讲述线程同步的问题,分别是买票、生产和消费

//买票正确程序
class A implements Runnable
{
	public int ticket = 100;
	String str = new String("hh");

	public void run()
	{
		while(true)
		{
			synchronized(str)
			{
				if (ticket > 0)
				{
					System.out.printf("%s线程正在卖出第%d张票\n", Thread.currentThread().getName(), tickets);
					--ticket;
				}
				else{
					break;
				}
			}
		}
	}
}

public class TestTickets
{
	public static void main(String[] args)
	{
		A aa = new A();
		Thread t1 = new Thread(aa);
		t1.start();

		Thread t2 = new Thread(aa);
		t2.start();
	}
}
//生产消费正确程序示例
class SynStack
{
	private char[] tickets = new char[6];
	private int cnt = 0; //表示数组有效元素的个数

	public synchronized void push(char ch)
	{
		while(cnt == tickets.length)
		{
			try{
				this.wait(); //a处  假设现在有两个线程P(生产)和C(消费),P生产已满,执行a处代码,线如阻塞状态
							//同时释放P线程对this的锁定,这时候C线程会得到this对象的标志位开始执行
			}catch(Exception e){
			}
		}
		this.notify(); //b处
		tickets[cnt] = ch;
		++cnt;
		System.out.printf("生产线程正在生产第%d个产品,产品是%s\n", cnt, ch);
	}

	public synchronized char pop()
	{
		char ch;

		while(cnt == 0)
		{
			try{
				this.wait();
			}catch(Exception e)
			{
			}
		}
		this.notify(); //c处  C线程执行完20行代码后,程序并不会立即转到P线程开始运行,因为C执行notify,只是叫醒P,让P从因为wait this对象而陷入阻塞的状态进入就绪状态,
					   //记住:一个线程notify,该线程并不会释放对this的锁定,只有C执行完d处的代码后,C才会释放对this的锁定,这时候C和P会同时争夺对this的锁定,具体执行哪个由系统调度器决定
		ch = tickets[cnt-1];
		System.out.printf("消费线程正在消费第%d个产品,产品是%s\n", cnt, ch);
		--cnt;
		return ch; // d处
	}
}
class Consumer implements Runnable
{
	private SynStack ss = null;

	public Consumer(SynStack ss)
	{
		this.ss = ss;
	}

	public void run()
	{
		for (int i=0; i<20; ++i)
		{
			ss.pop();
		}
	}
}

class Producer implements Runnable
{
	private SynStack ss = null;

	public Producer(SynStack ss)
	{
		this.ss = ss;
	}
	public void run()
	{
		//push('a'); //error
		char ch;
		for (int i=0; i<20; i++)
		{
			ch = (char)('a'+i);
			ss.push(ch);
		}
	}
}

public class TestPC
{
	public static void main(String[] agrs)
	{
		SynStack ss = new SynStack();
		Producer p = new Producer(ss);
		Consumer c = new Consumer(ss);
		Thread t1 = new Thread(p);
		t1.start();

		Thread t2 = new Thread(c);
		t2.start();
	}
}

上述程序要注意的问题
执行完c处的代码后,程序绝对不会立即切换到另一个线程。c处代码叫醒的是其他线程,叫醒的不是本线程。在最开始,P和C刚开始执行时,即便P没有wait,也可以在C中notify;即便C没有wait,也可以在P中notify

Synchronized关键字

synchronized可以用来修饰

  • 一个方法
  • 一个方法内部的某个代码块
synchronized修饰代码块
  • 格式:
synchronized(类对象名aa) 	//1行
{
	同步代码块 	//3行
}
  • 功能:
    • synchronized(类对象名aa) 的含义是:判断aa是否已经被其他线程霸占,如果发现已经被其他线程霸占,则当前线程陷入等待中,如果发现aa没有被其他线程霸占,则当前线程霸占住aa对象,并执行3行的同步代码块,在当前线程执行3行代码时,其他线程将无法再执行3行的代码(因为当前线程已经霸占了aa对象),当前线程执行完3行的代码后,会自动释放对aa对象的霸占,此时其他线程会相互竞争对aa的霸占,最终CPU会选择其中的某一个线程执行
    • 最终导致的结果是:一个线程正在操作某资源的时候,将不允许其他线程操作该资源,即一次只允许一个线程处理该资源
Synchronized修饰方法
  • Synchronized修饰一个方法时,实际霸占的是该方法的this指针所指向的对象
  • Synchronized修饰一个方法时,实际霸占的正在调用该方法的对象
  • 附注:霸占的专业术语叫锁定,霸占住的那个对象专业术语叫做监听器
同步概念

通常,一些同时运行的线程需要共享数据。在这种时候,每个线程就必须要考虑与其他一起共享数据的线程的状态与行为,否则的话就不能保证共享数据的一致性,从而也就不能保证程序的正确性。

class Stack{
	int index = 0;
	char[] data = new char[6];
	public void push(char c)
	{
		data[index] = c;
		index++;
	}
	public char pop()
	{
		index--;
		return data[index];
	}
}
  • 当有两个线程A和B同时使用了Stack类的一个对象时,现在要求:先把r存入Stack中,再将r取出来
    下面的步骤详细演示了AB线程不同步所带来的问题
    (1)操作之前,堆栈中有两个字符:
    data = | a | c | | | | | index = 2
    (2)A执行push中的第一条语句data[index] = 'r';
    data = | a | c | r | | | | index = 2 //2还没有被存入
    (3)A还没有执行index++语句,A被B中断,B执行pop()方法,返回’c’:
    data = | a | c | r | | | | index = 1 //取出的是'C'却不是'r'
    (4)A继续执行index++语句:
    · data = | a | c | r | | | | index = 2
  • 最终结果是:'r'没有被存入,取出的是'C'而不是'r'

notify 和 wait 方法

  • this.notify(); (只有在同步代码块或同步方法中才可以使用notify方法)
  • 功能:
    • 不是叫醒正在执行this.notify();的当前线程
    • 而是叫醒一个现在正在wait this对象的其他线程,如果有多个线程正在wait this对象,
    • 通常是叫醒最先wait this对象的线程,但具体是叫醒哪一个,这是由系统调度器控制,程序员无法控制

假设现在有T1T2T3T4四个线程,我们在T4线程中执行了aa.notify()语句,则即便此时T1T2T3没有一个线程因为wait aa对象而陷入阻塞状态,T4线程中执行aa.notify()方法时也不会有任何错误,下列程序就证明了这一点,执行aa.notify()方法时如果一个线程都没有叫醒,这是可以的

/*
	this.notify();
	功能:
		不是叫醒正在执行this.notify();的当前线程
		而是叫醒一个现在正在wait  this对象的其他线程,如果有多个线程正在wait this对象,
		通常是叫醒最先wait this对象的线程,但具体是叫醒哪一个,
		这是由系统调度器控制,程序员无法控制 		
	
	但是要注意:
		假设现在有T1 T2 T3 T4 四个线程
		
		在T4线程中调用了
			aa.notify()
		即便此时T1 T2 T3 没有一个线程因为wait aa对象而陷入阻塞状态, 
		T4线程中执行aa.notify方法时也不会有任何错误
		本程序就证明了这一点	
*/

public class TestNotify
{
	public static void main(String[] agrs)
	{
		A aa = new A();
		Thread tt = new Thread(aa);
		tt.start();
		System.out.println("哈哈");

		ThreadRun tr = new ThreadRun();
		new Thread(tr).start();
		new Thread(tr).start();
		new Thread(tr).start();
		new Thread(tr).start();
		new Thread(tr).start();
		new Thread(tr).start();
	}
}

class A implements Runnable
{
	public synchronized void run() //如果去掉了synchronized 则会报错,只有在同步代码块或同步方法中才可以使用notify方法
	{
		int cnt = 0;
		while(cnt < 20)
		{
			cnt++;
			System.out.println("AAAA" + cnt);
			try{
				this.notify();//如果此时有很多线程因为wait this对象而陷入阻塞状态,则叫醒其中的一个,至于具体是叫醒哪一个,则有系统调度器确定,程序员无法控制
							//本程序证明了,即便此时没有任何线程因为 wait this 对象而陷入阻塞状态,也可以调用notify方法
			}
			catch(IllegalMonitorStateException e)
			{
				System.out.println("在非同步方法 非同步代码块中调用了notify方法");
				System.exit(-1);
			}
		}
	}
}
class ThreadRun implements Runnable
{
	private int tickets = 100;
	public void run()
	{
		while(true)
		{
			synchronized (this)
			{
				if (tickets > 0)
				{
					System.out.printf("%s线程在卖出第%d张票!\n", Thread.currentThread().getName(), tickets);
					--tickets;
				}
				else
				{
					break;
				}
			}
		}
	}
}
public class SynTest
{
	public static void main(String[] args) throws Exception
	{
		SynStack ss = new SynStack();
		Runnable p = new Producer(ss);
		Runnable c = new Consumer(ss);
		Thread t1 = new Thread(p);
		Thread t2 = new Thread(c);
		t1.start();
		Thread.sleep(1000);
		t2.start();
	}
}

class SynStack  //支持多线程同步操作的堆栈的实现
{
	private int index = 0;
	private char[] data = new char[6];

	public synchronized void push(char ch)
	{
		while (index == data.length)
		{
			try
			{
				this.wait();
			}
			catch (InterruptedException e)
			{
				e.printStackTrace();
				System.exit(-1);
			}
		}
		this.notify();
		data[index] = ch;
		++index;
		System.out.println("生产:" + ch);
		System.out.printf("容器里有%d个字母\n\n", index);
	}

	public synchronized char pop()
	{
		while (index == 0)
		{
			try
			{
				this.wait();
			}
			catch (InterruptedException e)
			{
				e.printStackTrace();
				System.exit(-1);
			}
		}
		this.notify();
		index--;
		System.out.println("取出:" + data[index]);
		System.out.printf("容器里还有%d个字母!\n\n", index);
		return data[index];
	}
}

class Producer implements Runnable
{
	SynStack ss;
	public Producer(SynStack ss)
	{
		this.ss = ss;
	}
	public void run()
	{
		for (int i=0; i<20; i++)
		{
			char c = (char)('a'+i);
			ss.push(c);
			try
			{
				Thread.sleep((int)(Math.random()*20));
			}
			catch (InterruptedException e)
			{
			}
		}
	}
}
class Consumer implements Runnable
{
	SynStack ss;
	public Consumer(SynStack ss)
	{
		this.ss = ss;
	}
	public void run()
	{
		for (int i=0; i<20; i++)
		{
			char c = ss.pop();
			try
			{
				Thread.sleep((int)(Math.random()*1000));
			}
			catch (InterruptedException e)
			{
			}
		}
	}
}

notify 和 wait 方法 总结

  • aa.wait() 将执行aa.wait()的当前线程转入阻塞状态,让出CPU的控制权,释放对aa的锁定
  • aa.notify() 假设执行aa.notify()的当前线程为T1,如果当前时刻有其他线程因为执行了aa.wait()而陷入阻塞状态,则叫醒其中的一个,所谓叫醒某个线程就是令该线程从因为wait而线如阻塞的状态转入就绪状态
  • aa.notifyAll() 叫醒其他所有的因为执行了aa.wait()而陷入阻塞状态的线程

生产消费(经典问题)

一个仓库最多容纳6个产品,制造商现在要制造20件产品存入仓库,消费者要从仓库取出这20件产品来消费。制造商制造产品和消费者取出产品的速度很可能是不一样的,编程实现两者的同步。

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值