声明:由于学习所用环境为JDK1.8,所有java代码均在JDK1.8中测试通过,如果环境发生改变,可能会有错误发生!
一、通过实现Runnable接口创建线程
1、定义实现Runnable接口的类
Runnable接口中只有一个方法 public void run(); 用来定义线程运行体:
class MyRun implements Runnable{
public void run(){
线程执行的具体代码
}
}
创建线程的实例的时候将这个类的实例作为参数传递到线程实例内部。然后再启动:
Thread thread1 = new Thread(new MyRun());
thread1.start();
2、使用Runnable接口实现线程的优势
优势一:
避免了Java单继承的局限性,实现了Runnable接口的类还可以继承另外一个类或实现其他接口。
优势二:
使用实现Runnable接口的方式创建线程时可以为相同程序代码的多个线程提供共享的数据(资源共享)。
package runnable;
/*
* 通过实现Runnable接口的方式创建线程
* */
public class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 1; i <=100; i++) {
if(i%2==0){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"输出偶数:"+i);
}
}
}
}
package runnable;
public class TestRunnable {
public static void main(String[] args) {
MyRunnable mr=new MyRunnable();
Thread t=new Thread(mr,"子线程"); // 实例化一个线程对象,将Runnable接口的对象传入
// t.setDaemon(true); // 设置为“守护线程”(后台线程)
t.start(); // 启动子线程
System.out.println(Thread.currentThread().getName()+"线程运行完毕...");
}
}
二、多线程安全与安全问题当run()方法体内的代码操作到了成员变量(共享数据)时,就可能会出现多线程安全问题(线程不同步问题)。
编程技巧
在方法中尽量少操作成员变量(在不需要共享资源时),多使用局部变量。
1、线程同步
(1)在Java语言中,引入对象互斥锁的概念,保证共享数据操作的完整性。每个对象都对应于一个可称为“互斥锁”的标记,这个标记保证在任一时刻,只能有一个线程访问对象。
(2)关键字synchronized用来与对象的互斥锁关联。当某个对象用synchronized修饰时,表明该对象在任一时刻只能由一个线程访问(这个对象就变成了同步对象)。
(3)一个方法使用关键字synchronized修饰后,当一个线程A使用这个方法时,其他线程想使用这个方法就必须等待,直到线程A使用完该方法(前提是这些线程使用的是同一个同步对象)。
2、同步方法的同步对象
1.对于非静态方法来说,this当前对象充当了同步对象
package synchronizeddemo;
public class TicketRunnable implements Runnable{
private int ticket=5;
@Override
public void run() {
for (int i = 0; i < 100; i++) { // 故意使循环次数超过票的总数
sell();
}
}
// 对于非静态方法来说,this当前对象充当了同步对象
public synchronized void sell(){
if(ticket>0){ // 如果还有票,则卖票
try {
Thread.sleep(1000); // 休眠1秒,交出CPU
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println(Thread.currentThread().getName()+"卖了一张票,当前剩余票数:"+ticket);
}
}
}
package synchronizeddemo;
public class TestTicket {
public static void main(String[] args) {
TicketRunnable tr=new TicketRunnable();
new Thread(tr,"A窗口").start();
new Thread(tr,"B窗口").start();
new Thread(tr,"C窗口").start();
}
}
2.对于静态方法来说,同步对象是“类对象”,"类对象"代表的是这个类本身,所有通过类实例化的普通对象共享这个"类对象"
package staticsync;
public class PersonRunnable implements Runnable{
@Override
public void run() {
makeMoney();
}
// 静态方法的同步对象是“类对象”,"类对象"代表的是这个类本身,所有通过类实例化的普通对象共享这个"类对象"
public static synchronized void makeMoney(){
System.out.println(Thread.currentThread().getName()+"开始帮你赚钱...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"帮你赚了钱");
}
}
package staticsync;
public class TestSync {
public static void main(String[] args) {
PersonRunnable pr1=new PersonRunnable();
PersonRunnable pr2=new PersonRunnable();
PersonRunnable pr3=new PersonRunnable();
new Thread(pr1,"A线程").start();
new Thread(pr2,"B线程").start();
new Thread(pr3,"C线程").start();
}
}
三、使用synchronized关键字实现同步(买票问题)
1、synchronized的使用方法
(1)同步代码块:synchronized关键字放在对象前修饰一段代码
synchronized(同步对象){
需要同步的代码;
}
(2)同步方法:synchronized放在方法声明中
public synchronized void method(){
…
}
2、加上同步机制后,效率低的原因:
1. 会丧失Java多线程的并发优势。在执行到同步代码块(或同步方法)时,只能有一个线程执行,其他线程必须等待执行同步代码块(同步方法)的线程释放同步对象的锁。
2. 其他等待锁释放的线程会不断检查锁的状态。
四、死锁问题(系统“假死”现象)
死锁的原因:
线程1锁住资源A等待资源B,线程2锁住资源B等待资源A,两个线程都在等待自己需要的资源,而这些资源被另外的线程锁住,这些线程你等我,我等你,谁也不愿意让出资源,这样死锁就产生了。
典型举例:哲学家进餐问题
package deadlock;
// 叉子类
public class Fork {
public void forkSay(){
System.out.println("我拿到叉子了,请给我刀子...");
}
}
package deadlock;
// 刀子类
public class Knife {
public void knifeSay(){
System.out.println("我拿到刀子了,请给我叉子...");
}
}
package deadlock;
// "哲学家进餐"
public class PhilosopherRunnable implements Runnable{
private boolean flag=false;
private static Knife knife=new Knife(); // 刀子资源
private static Fork fork=new Fork(); // 叉子资源
public void setFlag(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if(flag){
synchronized(knife){
knife.knifeSay();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(fork){
fork.forkSay();
}
}
System.out.println(Thread.currentThread().getName()+"吃完饭了......");
}else{
synchronized(fork){
fork.forkSay();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized(knife){
knife.knifeSay();
}
}
System.out.println(Thread.currentThread().getName()+"吃完饭了......");
}
}
}
package deadlock;
public class Test {
public static void main(String[] args) {
PhilosopherRunnable p1=new PhilosopherRunnable();
p1.setFlag(true);
PhilosopherRunnable p2=new PhilosopherRunnable();
new Thread(p1,"尼采").start();
new Thread(p2,"苏格拉底").start();
}
}
解决方法:(1)加锁顺序一致
(2)加大锁的力度(不要套接锁)
五、Lock实现同步
java.util.concurrent.locks包下的相关接口与类1. Lock接口
通常使用Lock接口中的方法用来获取锁,其中lock()方法是使用得最多的一个方法。
2. ReentrantLock类(“可重入锁”)
ReentrantLock是实现了Lock接口的类
3. ReadWriteLock接口
包括了两个方法:
Lock readLock(); 用来获取“读锁”
Lock writeLock(); 用来获取“写锁”
4. ReentrantReadWriteLock类
是ReadWriteLock接口的实现类
package lock;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TicketLock implements Runnable{
private int ticket=5;
private Lock lock=new ReentrantLock();
@Override
public void run() {
for (int i = 0; i < 100; i++) {
lock.lock(); // 获取锁(更常用的方法)
//lock.tryLock();
try{
if(ticket>0){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ticket--;
System.out.println(Thread.currentThread().getName()+"卖了一张票,当前剩余票数:"+ticket);
}
}finally{
lock.unlock(); // 释放锁
}
}
}
}
package lock;
public class TestLock {
public static void main(String[] args) {
TicketLock tl=new TicketLock();
new Thread(tl,"线程A").start();
new Thread(tl,"线程B").start();
new Thread(tl,"线程C").start();
}
}
5、关于线程的读锁和写锁:如果有一个线程已经占用了读锁,则此时其他线程如果要申请写锁,则申请写锁的线程会一直等待释放读锁;但其他线程申请读锁是可以的。
如果有一个线程已经占用了写锁,则此时其他线程如果申请写锁或者读锁,则申请的线程会一直等待释放写锁。
package rwlock;
import java.util.Random;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class InfoRunnable implements Runnable {
private static int data;
private boolean flag=false;
private static ReadWriteLock rwl=new ReentrantReadWriteLock(); // 该属性对象可以获取"读锁"或"写锁"
public void setFlag(boolean flag) {
this.flag = flag;
}
@Override
public void run() {
if(flag){
for(int i=0;i<10;i++){
writeData();
}
}else{
for(int i=0;i<10;i++){
readData();
}
}
}
// 写数据方法
public static void writeData(){
try{
rwl.writeLock().lock(); // 获取"写锁"
System.out.println(Thread.currentThread().getName()+"准备写数据...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
data=new Random().nextInt(10); // 模拟"写数据"
System.out.println(Thread.currentThread().getName()+"写数据完毕。");
}finally{
rwl.writeLock().unlock(); // 释放"写锁"
}
}
// 读数据方法
public static void readData(){
try{
rwl.readLock().lock(); // 获取"读锁"
System.out.println(Thread.currentThread().getName()+"准备读取数据...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"读取到的数据是:"+data);
}finally{
rwl.readLock().unlock(); // 释放"读锁"
}
}
}
package rwlock;
public class TestInfo {
public static void main(String[] args) {
InfoRunnable info1=new InfoRunnable();
info1.setFlag(true);
new Thread(info1,"写入线程1").start();
new Thread(info1,"写入线程2").start();
InfoRunnable info2=new InfoRunnable();
new Thread(info2,"读取线程1").start();
InfoRunnable info3=new InfoRunnable();
new Thread(info3,"读取线程2").start();
}
}
Synchronized与Lock
(1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
(2) synchronized在发生异常时,会自动释放线程占有的锁;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
(3) Lock可以提高多个线程进行读操作的效率。